
Want to send emails from Power Automate but can’t or don’t want to use a service account?
Tired of dealing with:
By default most users will have the ability to go to https://entra.microsoft.com and log in with your user account.
Import-Module ExchangeOnlineManagement
Connect-ExchangeOnline -Device
New-ApplicationAccessPolicy
-AppId b9701c1e-1364-464d-93e4-01ae925e8d6c
-PolicyScopeGroupId PowerAutomateTest@Tweed.technology
-AccessRight RestrictAccess
-Description "Restrict this app to members of PowerAutomateTest@Tweed.technology"
Test allowed user:
Test-ApplicationAccessPolicy
-Identity testABC@Tweed.technology
-AppId b9701c1e-1364-464d-93e4-01ae925e8d6c
Result: AccessCheckResult : Granted
Test denied user:
Test-ApplicationAccessPolicy
-Identity demo@Tweed.technology
-AppId b9701c1e-1364-464d-93e4-01ae925e8d6c
Result: AccessCheckResult : Denied
Now let’s set up the custom connector in Power Automate:
https://make.powerautomate.com/
Want to send emails from Power Automate but can’t or don’t want to use a service account? Tired of dealing with Conditional Access Policies, password expirations, and frequent logins? Do you want a simple, secure, and scalable way to send emails without constantly re-confirming security information?
Good news! In this guide, I’ll show you how to send emails directly from Microsoft Exchange using only an App Registration. No service accounts, no password headaches. I’ll also share a Custom Connector to make it even easier, plus step-by-step instructions to:
For this section you will need the help of a Global Administrator
By default most users will have the ability to go to https://entra.microsoft.com and log in with your user account.
Next we are going to create an application registration so our Power Automate flow can talk directly to Exchange Online.
We then need to give our App Registration an API Permission, This will be the permission used to talk to Exchange Online.
NOTE: Yes this permission lets your send emails as anyone, however we will restrict this later. DO NOT MISS USE IT IS POSSIBLE TO TRACK WHERE AN EMAIL CAME FROM.
You will notice that the Grant admin consent is grayed out
This is Because we currently lack administrative roles for our account. For this next step you will need a Global Admin.
Ask a Global administrator to grant admin consent for you
Our next step is to limit what permissions our application will have within exchange, we have already said it will have mail.send
however without further limitation that ill allow the application to send emails from any users email account.
This Stage will need to be done by an Exchange Online Administrator.
Further reading: Further information of these next steps can be found here.
When we tell exchange that we want to restrict access in some way for an application registration, we need to first create a Mail-enabled Security group.
There are a few ways to do this, however today we are going to do it from the Microsoft 365 Admin center.
For this next step we are going to use some Powershell. You can do this from a location of your choice however today we are going to demo it from Cloud Shell.
Next we are going to create an application access policy using both the Application ID and the Mail-enabled security group we created in earlier steps.
New-ApplicationAccessPolicy -AppId b9701c1e-1364-464d-93e4-01ae925e8d6c -PolicyScopeGroupId PowerAutomateTest@Tweed.technology -AccessRight RestrictAccess -Description "Restrict this app to members of PowerAutomateTest@Tweed.technology"New-ApplicationAccessPolicy - This cmdlet creates a new application access policy in Microsoft 365.-AppId b9701c1e-1364-464d-93e4-01ae925e8d6c - Specifies the unique identifier (AppId) of the application for which the policy is being created.-PolicyScopeGroupId PowerAutomateTest@Tweed.technology - Defines the scope of the policy by specifying the group ID (email address) that the policy will apply to.-AccessRight RestrictAccess - Sets the access right for the policy. In this case, it restricts access.-Description "Restrict this app to members of PowerAutomateTest@Tweed.technology" - Provides a description for the policy, explaining its purpose.Install-Module -Name ExchangeOnlineManagement -Force.Our next step, is really to go back a stage and import and connect to Exchange Online.
To do this we need to:
Import-Module ExchangeOnlineManagementConnect-ExchangeOnline -Device.New-ApplicationAccessPolicy -AppId b9701c1e-1364-464d-93e4-01ae925e8d6c -PolicyScopeGroupId PowerAutomateTest@Tweed.technology -AccessRight RestrictAccess -Description "Restrict this app to members of PowerAutomateTest@Tweed.technology"ScopeName : Power Automate Test
ScopeIdentity : Power Automate Test20250209121934
Identity : 63759d9f-bfca-4f52-ae98-8f2f1d7bc173\b9701c1e-1364-464d-93e4-01ae925e8d6c:S-1-5-21-3787302941-3231517822-469913106-31437838;9
98e9d79-817d-41c9-87d8-d9c07f27f4b2
AppId : b9701c1e-1364-464d-93e4-01ae925e8d6c
ScopeIdentityRaw : S-1-5-21-3787302941-3231517822-469913106-31437838;998e9d79-817d-41c9-87d8-d9c07f27f4b2
Description : Restrict this app to members of PowerAutomateTest@Tweed.technology
AccessRight : RestrictAccess
ShardType : All
IsValid : True
ObjectState : Unchanged
We can now test using PowerShell, to see if it’s applied correctly.
To do this:
We are going to run the following command Test-ApplicationAccessPolicy -Identity testABC@Tweed.technology -AppId b9701c1e-1364-464d-93e4-01ae925e8d6c
If we break down this command:
Test-ApplicationAccessPolicy - This cmdlet tests an application access policy in Microsoft 365 to verify if a user has access.-Identity testABC@Tweed.technology - Specifies the identity (email address) of the user to test against the application access policy.-AppId b9701c1e-1364-464d-93e4-01ae925e8d6c - Specifies the unique identifier (AppId) of the application for which the policy is being tested.Running the command we get the following response:
AppId : b9701c1e-1364-464d-93e4-01ae925e8d6c
Mailbox : testABC
MailboxId : 75283b3b-609a-4c1c-b8b8-baa1342fdfa6
MailboxSid : S-1-5-21-3787302941-3231517822-469913106-31499791
AccessCheckResult : Granted
Let’s test this against a different email address that is not within the Mail-enabled security group by running Test-ApplicationAccessPolicy -Identity demo@Tweed.technology -AppId b9701c1e-1364-464d-93e4-01ae925e8d6c.
AppId : b9701c1e-1364-464d-93e4-01ae925e8d6c
Mailbox : demo
MailboxId : d2ca4050-f8a9-4986-b998-387603b466b6
MailboxSid : S-1-5-21-3787302941-3231517822-469913106-19344836
AccessCheckResult : Denied
We can see it has being Denied which is the response we expected.
First we need to find custom connectors in Power Automate.
To do this we need to:
https://make.powerautomate.com/Send email using Graph.https://raw.githubusercontent.com/itweedie/PowerPlatform-Send-Emails-from-Power-Automate-without-a-Service-Account/refs/heads/main/connector/shared_mightora-5fsend-20mail-20with-20graph-5fe07b0f04a8b0d4c3/apiDefinition.swagger.jsonhttps://graph.microsoft.com.In order to be able to send attachments we need to add some C# to our connector. If you are not going to be sending attachments you can skip this step.
To do this you need to:
public class Script : ScriptBase
{
public override async Task<HttpResponseMessage> ExecuteAsync()
{
// Read the request content as a string
var requestContentAsString = await this.Context.Request.Content.ReadAsStringAsync().ConfigureAwait(false);
// Parse the request content string into a JSON object
var requestContentAsJson = JObject.Parse(requestContentAsString);
// Modify the attachments array if it exists
List<string> attachmentFileTypes = new List<string>();
if (requestContentAsJson["message"]?["attachments"] is JArray attachments)
{
foreach (var attachment in attachments)
{
// Add the @odata.type element
attachment["@odata.type"] = "#microsoft.graph.fileAttachment";
}
}
// Set the modified JSON back to the request content
this.Context.Request.Content = CreateJsonContent(requestContentAsJson.ToString());
// Send the API request and get the response
var response = await this.Context.SendAsync(this.Context.Request, this.CancellationToken).ConfigureAwait(continueOnCapturedContext: false);
// Read the response content as a string
var responseContentAsString = await response.Content.ReadAsStringAsync().ConfigureAwait(false);
// Check if the response content is empty or null
if (string.IsNullOrEmpty(responseContentAsString))
{
// Set a default message if there is no response from the endpoint
responseContentAsString = "{\"message\": \"No response from the endpoint\"}";
}
else
{
try
{
// Try to parse the response content string into a JSON object
var responseContentAsJson = JObject.Parse(responseContentAsString);
// Convert the JSON object back to a string
responseContentAsString = responseContentAsJson.ToString();
}
catch (JsonReaderException)
{
// If parsing fails, set an error message with the invalid JSON response
responseContentAsString = $"{{\"message\": \"Invalid JSON response\", \"response\": \"{responseContentAsString}\"}}";
}
}
// Make a custom HTTP GET call to the developer messaging API
string developerMessage = "Failed to get updated developer message";
try
{
var request = (HttpWebRequest)WebRequest.Create("https://developer-message.mightora.io/api/HttpTrigger?appname=send-email-with-graph");
request.Method = "GET";
using (var developerResponse = (HttpWebResponse)request.GetResponse())
{
using (var streamReader = new StreamReader(developerResponse.GetResponseStream()))
{
var developerResponseContent = streamReader.ReadToEnd();
var developerResponseJson = JObject.Parse(developerResponseContent);
developerMessage = developerResponseJson["message"]?.ToString() ?? developerMessage;
}
}
}
catch
{
// If the GET request fails, developerMessage remains as the default failure message
}
// Create a JSON object to include the original request, the response content, and the developer message
var finalResponseContent = new JObject
{
["version"] = "1.2.0", // Add version number here
["responseContent"] = JObject.Parse(responseContentAsString),
["developerMessage"] = developerMessage
};
// Set the response content back to the JSON string
response.Content = CreateJsonContent(finalResponseContent.ToString());
// Return the response
return response;
}
private bool IsBinary(string content)
{
// Check if the content contains non-printable characters
foreach (char c in content)
{
if (char.IsControl(c) && c != '\r' && c != '\n' && c != '\t')
{
return true;
}
}
return false;
}
}
If you are planing to use this connector to send attachments, if the file is binary you will need to convert it to base64.
By following this guide, youโve successfully set up a secure, scalable way to send emails from Power Automateโwithout relying on service accounts, password resets, or Conditional Access headaches.
With your App Registration configured, permissions locked down, and custom connector deployed, you now have a robust method to send emails directly through Exchange Onlineโwhile ensuring access is tightly controlled.
โ
No Service Account Required โ Uses an App Registration instead.
โ
Secure and Controlled โ Email sending is restricted to specific mailboxes.
โ
Fully Automated โ No need to log in or manage passwords.
โ
Scalable and Future-Proof โ Works seamlessly across flows, reducing admin overhead.
Now that your setup is complete, you can start integrating email automation into your Power Automate workflows with confidence. Give it a try, experiment with different use cases, and let me know how it works for you!
๐ Happy automating! ๐