Skip to content

Commit c5f4bb1

Browse files
committed
Merge branch 'main' of https://github.com/GAM-team/GAM
2 parents 0fdcab4 + d8bf368 commit c5f4bb1

5 files changed

Lines changed: 179 additions & 24 deletions

File tree

src/GamCommands.txt

Lines changed: 13 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -4926,36 +4926,43 @@ gam print schema|schemas [todrive <ToDriveAttribute>*]
49264926
gam sendemail [recipient|to] <RecipientEntity>
49274927
[from <EmailAddress>] [mailbox <EmailAddress>] [replyto <EmailAddress>]
49284928
[cc <RecipientEntity>] [bcc <RecipientEntity>] [singlemessage]
4929-
[subject <String>] [<MessageContent>]
4929+
[subject <String>] [<MessageContent>] [html [<Boolean>]]
49304930
(replace <Tag> <String>)*
49314931
(replaceregex <RESearchPattern> <RESubstitution> <Tag> <String>)*
4932-
[html [<Boolean>]] (attach <FileName> [charset <Charset>])*
4932+
(attach <FileName> [charset <Charset>])*
49334933
(embedimage <FileName> <String>)*
49344934
[newuser <EmailAddress> firstname|givenname <String> lastname|familyname <string> password <Password>]
49354935
(<SMTPDateHeader> <Time>)* (<SMTPHeader> <String>)* (header <String> <String>)*
49364936
[threadid <String>]
49374937
gam <UserTypeEntity> sendemail recipient|to <RecipientEntity>
49384938
[replyto <EmailAddress>]
49394939
[cc <RecipientEntity>] [bcc <RecipientEntity>] [singlemessage]
4940-
[subject <String>] [<MessageContent>]
4940+
[subject <String>] [<MessageContent>] [html [<Boolean>]]
49414941
(replace <Tag> <String>)*
49424942
(replaceregex <RESearchPattern> <RESubstitution> <Tag> <String>)*
4943-
[html [<Boolean>]] (attach <FileName> [charset <Charset>])*
4943+
(attach <FileName> [charset <Charset>])*
49444944
(embedimage <FileName> <String>)*
49454945
[newuser <EmailAddress> firstname|givenname <String> lastname|familyname <string> password <Password>]
49464946
(<SMTPDateHeader> <Time>)* (<SMTPHeader> <String>)* (header <String> <String>)*
49474947
[threadid <String>]
49484948
gam <UserTypeEntity> sendemail from <EmailAddress>
49494949
[replyto <EmailAddress>]
49504950
[cc <RecipientEntity>] [bcc <RecipientEntity>] [singlemessage]
4951-
[subject <String>] [<MessageContent>]
4951+
[subject <String>] [<MessageContent>] [html [<Boolean>]]
49524952
(replace <Tag> <String>)*
49534953
(replaceregex <RESearchPattern> <RESubstitution> <Tag> <String>)*
4954-
[html [<Boolean>]] (attach <FileName> [charset <Charset>])*
4954+
(attach <FileName> [charset <Charset>])*
49554955
(embedimage <FileName> <String>)*
49564956
[newuser <EmailAddress> firstname|givenname <String> lastname|familyname <string> password <Password>]
49574957
(<SMTPDateHeader> <Time>)* (<SMTPHeader> <String>)* (header <String> <String>)*
49584958
[threadid <String>]
4959+
gam <UserTypeEntity> sendreply
4960+
(((query <QueryGmail> [querytime<String> <Date>]*) [or|and])+) | (ids <MessageIDEntity>)
4961+
[replyto <EmailAddress>]
4962+
[subject <String>] [<MessageContent>] [html [<Boolean>]]
4963+
(attach <FileName> [charset <CharSet>])*
4964+
(embedimage <FileName> <String>)*
4965+
(<SMTPDateHeader> <Time>)* (<SMTPHeader> <String>)* (header <String> <String>)*
49594966

49604967
# Shared Drives - Administrator
49614968

src/GamUpdate.txt

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,23 @@
1+
7.36.03
2+
3+
Added command to send email replies that causes Gmail to recognize the message
4+
in conversation mode for the user sending the reply and the user receiving the reply;
5+
GAM supplies the necessary headers and options.
6+
```
7+
gam <UserTypeEntity> sendreply
8+
(((query <QueryGmail> [querytime<String> <Date>]*) [or|and])+) | (ids <MessageIDEntity>)
9+
[replyto <EmailAddress>]
10+
[subject <String>] [<MessageContent>] [html [<Boolean>]]
11+
(attach <FileName> [charset <CharSet>])*
12+
(embedimage <FileName> <String>)*
13+
(<SMTPDateHeader> <Time>)* (<SMTPHeader> <String>)* (header <String> <String>)*
14+
15+
gam user user@domain.com sendreply query "rfc822MsgId:<CAAMmEdqj43...1OsQ@mail.gmail.com>" textmessage "Thanks for the information"
16+
gam user user@domain.com sendreply ids 19cfc3506c02c22b textmessage "Thanks for the information"
17+
```
18+
19+
* See: https://github.com/GAM-team/GAM/wiki/Send-Email#conversation-mode
20+
121
7.36.02
222

323
Added option `threadid <String>` to `gam [<UserTypeEntity>] sendemail` that causes Gmail to recognize the message

src/gam/__init__.py

Lines changed: 139 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -25,7 +25,7 @@
2525
"""
2626

2727
__author__ = 'GAM Team <google-apps-manager@googlegroups.com>'
28-
__version__ = '7.36.02'
28+
__version__ = '7.36.03'
2929
__license__ = 'Apache License 2.0 (http://www.apache.org/licenses/LICENSE-2.0)'
3030

3131
# pylint: disable=wrong-import-position
@@ -7341,7 +7341,8 @@ def _addEmbeddedImagesToMessage(message, embeddedImages):
73417341
# Send an email
73427342
def send_email(msgSubject, msgBody, msgTo, i=0, count=0, clientAccess=False, msgFrom=None, msgReplyTo=None,
73437343
html=False, charset=UTF8, attachments=None, embeddedImages=None,
7344-
msgHeaders=None, ccRecipients=None, bccRecipients=None, mailBox=None, threadId=None):
7344+
msgHeaders=None, ccRecipients=None, bccRecipients=None, mailBox=None, threadId=None,
7345+
action=Act.SENDEMAIL):
73457346
def checkResult(entityType, recipients):
73467347
if not recipients:
73477348
return
@@ -7397,12 +7398,13 @@ def cleanAddr(emailAddr):
73977398
if mailBox is None:
73987399
mailBox = msgFromAddr
73997400
_, mailBoxAddr = cleanAddr(mailBox)
7400-
action = Act.Get()
7401-
Act.Set(Act.SENDEMAIL)
7401+
parentAction = Act.Get()
7402+
Act.Set(action)
74027403
if not GC.Values[GC.SMTP_HOST]:
74037404
if not clientAccess:
74047405
userId, gmail = buildGAPIServiceObject(API.GMAIL, mailBoxAddr)
74057406
if not gmail:
7407+
Act.Set(parentAction)
74067408
return
74077409
else:
74087410
userId = mailBoxAddr
@@ -7444,7 +7446,7 @@ def cleanAddr(emailAddr):
74447446
server.quit()
74457447
except Exception:
74467448
pass
7447-
Act.Set(action)
7449+
Act.Set(parentAction)
74487450

74497451
def addFieldToFieldsList(fieldName, fieldsChoiceMap, fieldsList):
74507452
fields = fieldsChoiceMap[fieldName.lower()]
@@ -15385,32 +15387,35 @@ def getRecipients():
1538515387
return [normalizeEmailAddressOrUID(emailAddress, noUid=True, noLower=True) for emailAddress in recipients]
1538615388
return getNormalizedEmailAddressEntity(shlexSplit=True, noLower=True)
1538715389

15388-
# gam sendemail [recipient|to] <RecipientEntity> [from <EmailAddress>] [mailbox <EmailAddress>] [replyto <EmailAddress>]
15390+
# gam sendemail [recipient|to] <RecipientEntity>
15391+
# [from <EmailAddress>] [mailbox <EmailAddress>] [replyto <EmailAddress>]
1538915392
# [cc <RecipientEntity>] [bcc <RecipientEntity>] [singlemessage]
15390-
# [subject <String>] [<MessageContent>]
15393+
# [subject <String>] [<MessageContent>] [html [<Boolean>]]
1539115394
# (replace <Tag> <String>)*
1539215395
# (replaceregex <REMatchPattern> <RESubstitution> <Tag> <String>)*
15393-
# [html [<Boolean>]] (attach <FileName> [charset <CharSet>])*
15396+
# (attach <FileName> [charset <CharSet>])*
1539415397
# (embedimage <FileName> <String>)*
1539515398
# [newuser <EmailAddress> firstname|givenname <String> lastname|familyname <string> password <Password>]
1539615399
# (<SMTPDateHeader> <Time>)* (<SMTPHeader> <String>)* (header <String> <String>)*
1539715400
# [threadid <String>]
15398-
# gam <UserTypeEntity> sendemail recipient|to <RecipientEntity> [replyto <EmailAddress>]
15401+
# gam <UserTypeEntity> sendemail recipient|to <RecipientEntity>
15402+
# [replyto <EmailAddress>]
1539915403
# [cc <RecipientEntity>] [bcc <RecipientEntity>] [singlemessage]
15400-
# [subject <String>] [<MessageContent>]
15404+
# [subject <String>] [<MessageContent>] [html [<Boolean>]]
1540115405
# (replace <Tag> <String>)*
1540215406
# (replaceregex <REMatchPattern> <RESubstitution> <Tag> <String>)*
15403-
# [html [<Boolean>]] (attach <FileName> [charset <CharSet>])*
15407+
# (attach <FileName> [charset <CharSet>])*
1540415408
# (embedimage <FileName> <String>)*
1540515409
# [newuser <EmailAddress> firstname|givenname <String> lastname|familyname <string> password <Password>]
1540615410
# (<SMTPDateHeader> <Time>)* (<SMTPHeader> <String>)* (header <String> <String>)*
1540715411
# [threadid <String>]
15408-
# gam <UserTypeEntity> sendemail from <EmailAddress> [replyto <EmailAddress>]
15412+
# gam <UserTypeEntity> sendemail from <EmailAddress>
15413+
# [replyto <EmailAddress>]
1540915414
# [cc <RecipientEntity>] [bcc <RecipientEntity>] [singlemessage]
15410-
# [subject <String>] [<MessageContent>]
15415+
# [subject <String>] [<MessageContent> ][html [<Boolean>]]
1541115416
# (replace <Tag> <String>)*
1541215417
# (replaceregex <REMatchPattern> <RESubstitution> <Tag> <String>)*
15413-
# [html [<Boolean>]] (attach <FileName> [charset <CharSet>])*
15418+
# (attach <FileName> [charset <CharSet>])*
1541415419
# (embedimage <FileName> <String>)*
1541515420
# [newuser <EmailAddress> firstname|givenname <String> lastname|familyname <string> password <Password>]
1541615421
# (<SMTPDateHeader> <Time>)* (<SMTPHeader> <String>)* (header <String> <String>)*
@@ -15531,6 +15536,125 @@ def doSendEmail(users=None):
1553115536
attachments=attachments, embeddedImages=embeddedImages, msgHeaders=msgHeaders, mailBox=mailBox, threadId=threadId)
1553215537
Ind.Decrement()
1553315538

15539+
# gam <UserTypeEntity> sendreply
15540+
# (((query <QueryGmail> [querytime<String> <Date>]*) [or|and])+) | (ids <MessageIDEntity>)
15541+
# [replyto <EmailAddress>]
15542+
# [subject <String>] [<MessageContent>] [html [<Boolean>]]
15543+
# (attach <FileName> [charset <CharSet>])*
15544+
# (embedimage <FileName> <String>)*
15545+
# (<SMTPDateHeader> <Time>)* (<SMTPHeader> <String>)* (header <String> <String>)*
15546+
def doSendReply(users):
15547+
def _getHeaderValue(name):
15548+
for header in messageInfo['payload']['headers']:
15549+
if name == header['name']:
15550+
return _decodeHeader(header['value'])
15551+
return ''
15552+
15553+
notify = {'subject': '', 'message': '', 'html': False, 'charset': UTF8}
15554+
query = ''
15555+
queryTimes = {}
15556+
messageIds = []
15557+
msgHeaders = {}
15558+
msgReplyTo = None
15559+
attachments = []
15560+
embeddedImages = []
15561+
while Cmd.ArgumentsRemaining():
15562+
myarg = getArgument()
15563+
if myarg == 'query':
15564+
selectLocation = Cmd.Location()
15565+
if query:
15566+
query += ' '
15567+
query += f'({getString(Cmd.OB_QUERY)})'
15568+
elif myarg.startswith('querytime'):
15569+
queryTimes[myarg] = getDateOrDeltaFromNow().replace('-', '/')
15570+
elif myarg in {'or', 'and'}:
15571+
if query:
15572+
query += f' {myarg.upper()}'
15573+
elif myarg == 'ids':
15574+
selectLocation = Cmd.Location()
15575+
messageIds = getEntityList(Cmd.OB_MESSAGE_ID)
15576+
elif myarg == 'subject':
15577+
notify['subject'] = getString(Cmd.OB_STRING)
15578+
elif myarg in SORF_MSG_FILE_ARGUMENTS:
15579+
notify['message'], notify['charset'], notify['html'] = getStringOrFile(myarg)
15580+
elif myarg == 'replyto':
15581+
msgReplyTo = getString(Cmd.OB_EMAIL_ADDRESS)
15582+
elif myarg == 'html':
15583+
notify['html'] = getBoolean()
15584+
elif myarg == 'attach':
15585+
attachments.append((getFilename(), getCharSet()))
15586+
elif myarg == 'embedimage':
15587+
embeddedImages.append((getFilename(), getString(Cmd.OB_STRING)))
15588+
elif myarg in SMTP_HEADERS_MAP:
15589+
if myarg in SMTP_DATE_HEADERS:
15590+
msgDate, _, _ = getTimeOrDeltaFromNow(True)
15591+
msgHeaders[SMTP_HEADERS_MAP[myarg]] = formatdate(time.mktime(msgDate.timetuple()) + msgDate.microsecond/1E6, True)
15592+
else:
15593+
msgHeaders[SMTP_HEADERS_MAP[myarg]] = getString(Cmd.OB_STRING)
15594+
elif myarg == 'header':
15595+
header = getString(Cmd.OB_STRING, minLen=1)
15596+
msgHeaders[SMTP_HEADERS_MAP.get(header.lower(), header)] = getString(Cmd.OB_STRING)
15597+
else:
15598+
unknownArgumentExit()
15599+
if query and messageIds:
15600+
Cmd.SetLocation(selectLocation-1)
15601+
usageErrorExit(Msg.ARE_MUTUALLY_EXCLUSIVE.format('query <QueryGmail>', 'ids <MessageIDEntity>'))
15602+
notify['message'] = notify['message'].replace('\r', '').replace('\\n', '\n')
15603+
i, count, users = getEntityArgument(users)
15604+
for user in users:
15605+
i += 1
15606+
user, gmail = buildGAPIServiceObject(API.GMAIL, user, i, count)
15607+
if not gmail:
15608+
continue
15609+
try:
15610+
if query:
15611+
printGettingAllEntityItemsForWhom(Ent.MESSAGE, user, i, count, query=query)
15612+
listResult = callGAPIpages(gmail.users().messages(), 'list', 'messages',
15613+
pageMessage=getPageMessageForWhom(),
15614+
throwReasons=GAPI.GMAIL_THROW_REASONS+GAPI.GMAIL_LIST_THROW_REASONS,
15615+
userId='me', q=query, fields='nextPageToken,messages(id)',
15616+
maxResults=GC.Values[GC.MESSAGE_MAX_RESULTS])
15617+
messageIds = [message['id'] for message in listResult]
15618+
except (GAPI.failedPrecondition, GAPI.permissionDenied, GAPI.invalid, GAPI.invalidArgument) as e:
15619+
entityActionFailedWarning([Ent.USER, user], str(e), i, count)
15620+
continue
15621+
except GAPI.serviceNotAvailable:
15622+
userGmailServiceNotEnabledWarning(user, i, count)
15623+
continue
15624+
jcount = len(messageIds)
15625+
if jcount == 0:
15626+
entityNumEntitiesActionNotPerformedWarning([Ent.USER, user], Ent.MESSAGE, jcount, Msg.NO_ENTITIES_MATCHED.format(Ent.Plural(Ent.MESSAGE)), i, count)
15627+
setSysExitRC(NO_ENTITIES_FOUND_RC)
15628+
continue
15629+
entityPerformActionModifierNumItems([Ent.USER, user], Act.MODIFIER_TO, jcount, Ent.RECIPIENT, i, count)
15630+
Ind.Increment()
15631+
j = 0
15632+
for messageId in messageIds:
15633+
j += 1
15634+
try:
15635+
messageInfo = callGAPI(gmail.users().messages(), 'get',
15636+
throwReasons=GAPI.GMAIL_THROW_REASONS+[GAPI.NOT_FOUND, GAPI.INVALID_MESSAGE_ID],
15637+
userId='me', id=messageId, fields='id,threadId,payload(headers)')
15638+
threadId = messageInfo['threadId']
15639+
msgHeaders['References'] = msgHeaders['In-Reply-To'] = _getHeaderValue('Message-ID')
15640+
msgSubject = notify['subject'] if notify['subject'] else f"Re: {_getHeaderValue('Subject')}"
15641+
recipient = _getHeaderValue('From')
15642+
send_email(msgSubject, notify['message'], recipient, j, jcount,
15643+
msgFrom=user, msgReplyTo=msgReplyTo, html=notify['html'], charset=notify['charset'],
15644+
attachments=attachments, embeddedImages=embeddedImages, msgHeaders=msgHeaders, threadId=threadId,
15645+
action=Act.SENDREPLY)
15646+
except GAPI.notFound:
15647+
entityActionFailedWarning([Ent.USER, user, Ent.MESSAGE, messageId], Msg.DOES_NOT_EXIST, j, jcount)
15648+
except GAPI.invalidMessageId:
15649+
entityActionFailedWarning([Ent.USER, user, Ent.MESSAGE, messageId], Msg.INVALID_MESSAGE_ID, j, jcount)
15650+
except (GAPI.failedPrecondition, GAPI.permissionDenied, GAPI.invalid, GAPI.invalidArgument) as e:
15651+
entityActionFailedWarning([Ent.USER, user], str(e), i, count)
15652+
break
15653+
except GAPI.serviceNotAvailable:
15654+
userGmailServiceNotEnabledWarning(user, i, count)
15655+
break
15656+
Ind.Decrement()
15657+
1553415658
ADDRESS_FIELDS_PRINT_ORDER = ['contactName', 'organizationName', 'addressLine1', 'addressLine2', 'addressLine3', 'locality', 'region', 'postalCode', 'countryCode']
1553515659

1553615660
def _showCustomerAddressPhoneNumber(customerInfo):
@@ -80879,6 +81003,7 @@ def processResourcesCommands():
8087981003
'profile': (Act.SET, setProfile),
8088081004
'sendas': (Act.ADD, createUpdateSendAs),
8088181005
'sendemail': (Act.SENDEMAIL, doSendEmail),
81006+
'sendreply': (Act.SENDREPLY, doSendReply),
8088281007
'signature': (Act.SET, setSignature),
8088381008
'signout': (Act.SIGNOUT, signoutTurnoff2SVUsers),
8088481009
'turnoff2sv': (Act.TURNOFF2SV, signoutTurnoff2SVUsers),

src/gam/gamlib/glaction.py

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
# -*- coding: utf-8 -*-
22

3-
# Copyright (C) 2024 Ross Scroggs All Rights Reserved.
3+
# Copyright (C) 2026 Ross Scroggs All Rights Reserved.
44
#
55
# All Rights Reserved.
66
#
@@ -107,6 +107,7 @@ class GamAction():
107107
SAVE = 'save'
108108
SEND = 'send'
109109
SENDEMAIL = 'snem'
110+
SENDREPLY = 'sner'
110111
SET = 'set '
111112
SETUP = 'setu'
112113
SHARE = 'shar'
@@ -225,6 +226,7 @@ class GamAction():
225226
SAVE: ['Saved', 'Save'],
226227
SEND: ['Sent', 'Send'],
227228
SENDEMAIL: ['Email Sent', 'Send Email'],
229+
SENDREPLY: ['Reply Sent', 'Send Reply'],
228230
SET: ['Set', 'Set'],
229231
SETUP: ['Set Up', 'Set Up'],
230232
SHARE: ['Shared', 'Share'],

wiki/Send-Email.md

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -465,11 +465,12 @@ gam user recipient@domain.com sendemail to sender@domain.com references "<CAAMab
465465
If you want to have Gmail recognize the reply in conversation mode in the Sent folder of the original recipient,
466466
you must include `threadid <String>`; you can get the 'threadId` with:
467467
```
468-
gam user recipient@domain.com show messages query "rfc822MsgId:<CAAMabc...XYZQ@mail.gmail.com>"
468+
gam user recipient@domain.com show threads query "rfc822MsgId:<CAAMabc...XYZQ@mail.gmail.com>"
469469
Getting all Messages that match query ((rfc822MsgId:<CAAMabc...XYZQ@mail.gmail.com>)) for recipient@domain.com
470470
Got 1 Message that matched query ((rfc822MsgId:<CAAMabc...XYZQ@mail.gmail.com>)) for recipient@domain.com...
471-
User: recipient@domain.com, Show 1 Message
472-
Message: 19cfd414fe48430d
471+
User: recipient@domain.com, Show 1 Thread
472+
Thread: 19cfd414fe48430d
473+
Message: 19cfd414fe48430d
473474
...
474475
475476
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

0 commit comments

Comments
 (0)