Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Email communication service - Send command - added an optional parameter wait-until and added a new command get email request status. #8005

Open
wants to merge 3 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions src/communication/HISTORY.rst
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,11 @@
Release History
===============
1.10.0
++++++
* Adding new parameter waituntil to Email communication send mail.
* Adding new command get send status to Email communication.

1.9.3
++++++
* Update Email service create - Global as the default value for Location
Expand Down
14 changes: 14 additions & 0 deletions src/communication/azext_communication/manual/_help.py
Original file line number Diff line number Diff line change
Expand Up @@ -575,3 +575,17 @@
text: |-
az communication email send --sender "NoReply@contoso.com" --subject "Contoso Update" --to "user1@user1-domain.com" "user2@user2-domain.com" --text "Hello valued client. There is an update."
"""

helps['communication email status'] = """
type: group
short-summary: Commands to get the status of emails previously sent using Azure Communication Services Email service.
"""

helps['communication email status get'] = """
type: command
short-summary: "Get status of an email previously sent."
examples:
- name: Get status of an email
text: |-
az communication email status get --operation-id "01234567-89ab-cdef-0123-012345678901" --connection-string "endpoint=XXXXXXXXXXXXXXXX;accesskey=XXXXXXXXXXXXXXXXXXXXXX"
"""
10 changes: 10 additions & 0 deletions src/communication/azext_communication/manual/_params.py
Original file line number Diff line number Diff line change
Expand Up @@ -284,3 +284,13 @@ def _load_email_arguments(self):
' Required for each attachment. Known values are: avi, bmp, doc, docm,'
' docx, gif, jpeg, mp3, one, pdf, png, ppsm, ppsx, ppt, pptm, pptx,'
' pub, rpmsg, rtf, tif, txt, vsd, wav, wma, xls, xlsb, xlsm, and xlsx')
c.argument('waitUntil', options_list=['--wait-until'],
arg_type=get_enum_type(['started', 'completed', '1', '0']),
help='Indicates whether to wait until the server operation is started or completed. '
'Accepted values are: started, completed, 1, 0.')

with self.argument_context('communication email status get') as c:
c.argument('operation_id', options_list=['--operation-id'], type=str,
help='System generated message id (GUID) returned from a previous call to send email')
c.argument('connection_string', options_list=['--connection-string'], type=str,
help='Connection string for Azure Communication Service. Must be provided.')
2 changes: 2 additions & 0 deletions src/communication/azext_communication/manual/commands.py
Original file line number Diff line number Diff line change
Expand Up @@ -124,3 +124,5 @@ def _load_email_command_table(self):

with self.command_group('communication email', client_factory=cf_communication_email) as g:
g.communication_custom_command('send', 'communication_email_send', email_arguments)
with self.command_group('communication email', is_preview=True) as g:
g.communication_custom_command('status get', 'communication_email_get_status', email_arguments)
123 changes: 108 additions & 15 deletions src/communication/azext_communication/manual/custom.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,12 @@
import sys
import ast
from azure.core.exceptions import HttpResponseError
import base64
import hashlib
import hmac
import requests
from datetime import datetime
from urllib.parse import urlparse


def communication_identity_create_user(client):
Expand Down Expand Up @@ -315,7 +321,7 @@ def communication_rooms_remove_participants(client, room_id, participants):


def __get_attachment_content(filename, filetype):
import base64

import json
import os

Expand All @@ -334,6 +340,25 @@ def __get_attachment_content(filename, filetype):
return json.dumps(attachment)


def prepare_attachments(attachments, attachment_types):
from knack.util import CLIError

attachments_list = []
if attachments is None and attachment_types is None:
attachments_list = None
elif attachments is None or attachment_types is None:
raise CLIError('Number of attachments and attachment-types should match.')
elif len(attachments) != len(attachment_types):
raise CLIError('Number of attachments and attachment-types should match.')
else:
all_attachments = attachments[0].split(',')
all_attachment_types = attachment_types[0].split(',')
for i, attachment in enumerate(all_attachments):
attachments_list.append(__get_attachment_content(attachment, all_attachment_types[i]))

return attachments_list


def communication_email_send(client,
subject,
sender,
Expand All @@ -346,10 +371,12 @@ def communication_email_send(client,
recipients_bcc=None,
reply_to=None,
attachments=None,
attachment_types=None):
attachment_types=None,
waitUntil='completed'):

import json
from knack.util import CLIError
import uuid

try:

Expand All @@ -363,18 +390,7 @@ def communication_email_send(client,
else:
priority = '3'

attachments_list = []
if attachments is None and attachment_types is None:
attachments_list = None
elif attachments is None or attachment_types is None:
raise CLIError('Number of attachments and attachment-types should match.')
elif len(attachments) != len(attachment_types):
raise CLIError('Number of attachments and attachment-types should match.')
else:
all_attachments = attachments[0].split(',')
all_attachment_types = attachment_types[0].split(',')
for i, attachment in enumerate(all_attachments):
attachments_list.append(__get_attachment_content(attachment, all_attachment_types[i]))
attachments_list = prepare_attachments(attachments, attachment_types)

message = {
"content": {
Expand All @@ -400,7 +416,84 @@ def communication_email_send(client,
}
}

return client.begin_send(message)
operationId = str(uuid.uuid4())
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Do we accept an operation id from the client today?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We are accepting an operation ID from the client. However, to support the "wait until" parameter, the operation ID is not displayed in the result when the operation is initiated. Therefore, we are generating the operation ID programmatically.


poller = client.begin_send(message, operation_id=operationId)

if waitUntil == 'started' or waitUntil == '1':
print("Email send started")
print(f"Operation id : {operationId}, status : {poller.status()} ")
elif waitUntil == 'completed' or waitUntil == '0':
# Wait until the email is sent and get the result
return poller
else:
raise ValueError("Invalid value for waitUntil. Expected 'started' or 'completed'.")

except HttpResponseError:
raise
except Exception as ex:
sys.exit(str(ex))


def parse_connection_string(connection_string):
"""
Parse the connection string to extract the endpoint and API key.
"""
params = {}
for item in connection_string.split(';'):
key, value = item.split('=', 1)
params[key.strip()] = value.strip()

api_endpoint = params.get('endpoint')
api_key = params.get('accesskey')

if not api_endpoint or not api_key:
raise ValueError("Connection string is missing required parameters.")

return api_endpoint, api_key


def create_signature_header(method, url, host, api_key):

date_str = datetime.utcnow().strftime('%a, %d %b %Y %H:%M:%S GMT')

hashed_body = hashlib.sha256(b'').digest()
hashed_body_base64 = base64.b64encode(hashed_body).decode('utf-8')

string_to_sign = f"{method}\n{url}\n{date_str};{host};{hashed_body_base64}"
signing_key = base64.b64decode(api_key)
signature = hmac.new(signing_key, string_to_sign.encode('utf-8'), hashlib.sha256).digest()
signature_base64 = base64.b64encode(signature).decode('utf-8')

headers = {
'x-ms-date': date_str,
'x-ms-content-sha256': hashed_body_base64,
'Authorization': f"HMAC-SHA256 SignedHeaders=x-ms-date;host;x-ms-content-sha256&Signature={signature_base64}"
}

return headers


def communication_email_get_status(connection_string, operation_id):
try:
api_endpoint, api_key = parse_connection_string(connection_string)

status_endpoint = f"{api_endpoint}emails/operations/{operation_id}?api-version=2023-03-31"

method = "GET"
url = f'/emails/operations/{operation_id}?api-version=2023-03-31'
parsed_url = urlparse(api_endpoint)
host = parsed_url.netloc

headers = create_signature_header(method, url, host, api_key)

response = requests.get(status_endpoint, headers=headers)

if response.status_code == 200:
return response.json()

response.raise_for_status()

except HttpResponseError:
raise
except Exception as ex:
Expand Down
2 changes: 1 addition & 1 deletion src/communication/azext_communication/version.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@
# --------------------------------------------------------------------------------------------


VERSION = '1.9.3'
VERSION = '1.10.0'


def cli_application_id():
Expand Down
Loading