5
votes

In our application, we need to send notifications to users by email for various event triggers.

I'm able to send email if I send as "Me" the current user, but trying to send as another user account returns an error message and I'd prefer it if notifications didn't come users' themselves and may contain info we don't want floating around in Sent folders.

What works:

await graphClient.Me.SendMail(email, SaveToSentItems: false).Request().PostAsync();

What doesn't work:

string FromUserEmail = "[email protected]";
await graphClient.Users[FromUserEmail].SendMail(email, SaveToSentItems: false).Request().PostAsync();

Also tried using the user object id directly:

await graphClient.Users["cd8cc59c-0815-46ed-aa45-4d46c8a89d72"].SendMail(email, SaveToSentItems: false).Request().PostAsync();

My application has permissions for the Graph API to "Send mail as any user" enabled and granted by the owner/administrator.

The error message returned by the API:

Code: ErrorFolderNotFound Message: The specified folder could not be found in the store.

I thought this error might have been because the notifications account didn't have a sent folder, so I set the SaveToSentItems value to false, but I still get the same error.

Are there any settings I need to check on the account itself to allow the app to send mail on this account or should this work?

I have checked out the documentation here: https://developer.microsoft.com/en-us/graph/docs/api-reference/v1.0/api/user_sendmail

Which appears to support what I'm trying to do, but doesn't reference any folder except for the sent items folder which I'm telling the API not to save to anyway.

We aren't intending to impersonate any actual user here, just send notification emails from within the app from this specific account (which I know is technically impersonation, but not of a real entity).

3
Is "[email protected]" an actual separate user account in your tenant, or is it a shared mailbox? And to clarify, you're logging in as a user in your app and requesting the Mail.Send.Shared permission scope?Jason Johnston
"Notifications" is a fully fledged user account in our tenancy, not a shared inbox. Users will be logging into the application as themselves.Stuart Frankish
Ok. Where is your app registered, Azure portal (portal.azure.com) or the App Registration Portal (apps.dev.microsoft.com)?Jason Johnston
Registered on the Azure Portal > Azure Active Directory > App Registrations. It has Microsoft Azure Active Directory and Graph API permissions administratively granted, with Send mail as any user set on.Stuart Frankish
Ok, I'm going to check it on my side. One thing for you to check is your access token. If you parse it at jwt.io, do you see Mail.Send.Shared in the scp claim?Jason Johnston

3 Answers

5
votes

Whenever you are using delegated permissions (i.e. when a user is logged in), even though your admin has consented to the Mail.Send.Shared, it does NOT grant access to all mailboxes in the tenant. These OAuth permissions do not override the permissions (and restrictions) in place for the user.

If the user is not already configured with permissions to be able to "Send As" the [email protected] user, then you'll see this error.

To make it work, you'd need to actually grant "Send As" rights to all users that will be using your application.

This is a subtle thing, and granted it's a bit confusing. In the Azure portal, the permissions have slightly different descriptions, depending on if you're looking at the Application Permissions or the Delegated Permissions.

  • Application: Send mail as any user
  • Delegated: Send mail on behalf of others

Since you're using delegated, the permission doesn't allow you to send as any user, only send on behalf of any folks that the logged on user has rights to send as.

Another approach you could use here to avoid having to grant these rights to all users (which would allow them to send via Outlook, etc.) would be to have your backend app use the client credentials flow to get an app-only token. In that case, the app itself would have the permission to send as any user.

5
votes

So like Schwarzie2478 we used a [email protected] address. But our AD is federated which means you can't use Username\Password auth and we didn't want to use the Application Mail.Send permission since it literally can send as anyone and there is no way IT Security would let that fly. So we used Windows Authentication instead.

This requires that you grant consent to the app to use the mail.send and user.read delegate permissions by going to https://login.microsoftonline.com/{tenantId}/oauth2/v2.0/authorize?client_id={clientId}&response_type=code&scope=user.read%20mail.send and logging in with the windows user that the app will run as.

More info on using windows auth here: https://github.com/AzureAD/microsoft-authentication-library-for-dotnet/wiki/Integrated-Windows-Authentication

// method call
var t = SendEmailUsingGraphAPI();
t.Wait();

// method
static async Task<Boolean> SendEmailUsingGraphAPI() {

    // AUTHENTICATION
    var tenantID = "YOUR_TENANT_ID"; //azure ad tenant/directory id
    var clientID = "YOUR_APPS_CLIENT_ID"; // registered app clientID
    var scopes = "user.read mail.send";  // DELEGATE permissions that the request will need

    string authority = $"https://login.microsoftonline.com/{tenantID}";
    string[] scopesArr = new string[] { scopes };

    try {
        IPublicClientApplication app = PublicClientApplicationBuilder
                .Create(clientID)
                .WithAuthority(authority)
                .Build();

        var accounts = await app.GetAccountsAsync();

        AuthenticationResult result = null;
        if (accounts.Any()) {
            result = await app.AcquireTokenSilent(scopesArr, accounts.FirstOrDefault())
                .ExecuteAsync();
        }
        else {
            // you could acquire a token by username/password authentication if you aren't federated.
            result = await app.AcquireTokenByIntegratedWindowsAuth(scopesArr)
                //.WithUsername(fromAddress)
                .ExecuteAsync(CancellationToken.None);
        }

        Console.WriteLine(result.Account.Username);


        // SEND EMAIL
        var toAddress = "EMAIL_OF_RECIPIENT";
        var message = "{'message': {'subject': 'Hello from Microsoft Graph API', 'body': {'contentType': 'Text', 'content': 'Hello, World!'}, 'toRecipients': [{'emailAddress': {'address': '" + result.Account.Username + "'} } ]}}";

        var restClient = new RestClient("https://graph.microsoft.com/v1.0/users/" + result.Account.Username + "/sendMail");
        var request = new RestRequest(Method.POST);
        request.AddHeader("Content-Type", "application/json");

        request.AddHeader("Authorization", "Bearer " + result.AccessToken);
        request.AddParameter("", message, ParameterType.RequestBody);
        IRestResponse response = restClient.Execute(request);
        Console.WriteLine(response.Content);

    }
    catch (Exception e) {
        Console.WriteLine(e.Message);
        throw e;
    }

    return true;
}
3
votes

I don't know what others will have done for this, but I contacted Microsoft about this exact scenario: I want to send a mail as a fixed user ( [email protected]) which has a mailbox in Azure. I want to send this mail from different applications or services.

The person there told me that sending a mail with no user logging in, is only possible with an delegated user token.

So we configured our application as an Native application in Azure like for mobile apps. Logging in for this application with the technical user during a setup phase gives me a delegated user token for that specific user which can be stored in a mailing service or component. This token does not expire ( at least not until the security changes of the user like password or something) and can be used to call the graph api to send mails when you give permission for this account to be sending mails from.

Next to that we even associated other shared mailboxes to this accounts to be able to send mails for those mailboxes too.

Documentation:

First You need a native app registration in Azure ( not an Web API):

https://docs.microsoft.com/en-us/azure/active-directory/develop/native-app

This app only requires an one-time login and approval from an user to get a token which can represent that user indefinitly. We set up a mail user account to be used for this. That token is then used to get access token to Graph Api for sending mails and such

Token Handling example:

https://github.com/AzureAD/microsoft-authentication-library-for-dotnet/wiki/token-cache-serialization

With an identitytoken stored ( usually a .cache file somewhere) you can request an accesstoken:

Identity Client:

https://docs.microsoft.com/en-us/dotnet/api/microsoft.identity.client.publicclientapplication?view=azure-dotnet

 _clientApp = new PublicClientApplication(ClientId, "https://login.microsoftonline.com/{xxx-xxx-xx}, usertoken,...

authResult = await _clientApp.AcquireTokenSilentAsync(scopes,...

        private static string graphAPIEndpoint = "https://graph.microsoft.com/v1.0/me";

    //Set the scope for API call to user.read
    private static string[] scopes = new string[] { "user.read", "mail.send" };
    private const string GraphApi = "https://graph.microsoft.com/";

            var graphclient = new GraphServiceClient($"{GraphApi}/beta",
                       new DelegateAuthenticationProvider(
                           (requestMessage) =>
                           {                                  
                                   // inject bearer token for auth
                                   requestMessage.Headers.Authorization = new AuthenticationHeaderValue("bearer", authResult.AccessToken);
                               return Task.FromResult(0);
                           }));

        var sendmail = graphclient.Users[User].SendMail(mail), true);
        try
        {
            await sendmail.Request().PostAsync();
        }
        catch (Exception e)
        {