PC SOFT

PROFESSIONAL NEWSGROUPS
WINDEVWEBDEV and WINDEV Mobile

Home → WINDEV 2024 → Office 365 OAuth 2.0 avec les API Microsoft Graph
Office 365 OAuth 2.0 avec les API Microsoft Graph
Started by Jérémie, Oct., 17 2023 10:07 PM - 8 replies
Registered member
19 messages
Posted on October, 17 2023 - 10:07 PM
Bonjour,

Je recherche depuis deux jours sans trouver de solution qui me convienne et sur le forum il y a plusieurs discussions ouvertes sur le sujet, mais sans vraiment de réponse claire... et sans doute lié au différent changement du coter de Microsoft.

Je voudrais pouvoir envoyer des mails depuis mon application de bureau de manière automatique via une session SMTP, jusque-là pas bien compliquer avec le login et le mot de passe aucun problème, par contre pour passer à une authentification oAuth 2.0 c'est la galère.

Sur Microsoft Entra :
- j'ai inscrit une nouvelle application
- dans "API autorisées", j'ai activé les API du Microsoft Graph :
-- SMTP.Send Type Déléguée
-- offline_access Type Déléguée
-- Mail.Send Type Déléguée
-- Mail.Send Type Application
- dans "Authentification" j'ai ajouté une URI de redirection : http://localhost:9874
- dans "Certificats & secrets" j'ai ajouté un secret client valide pour 2 ans (qui est la durée max)

Mon code :
À noter que je récupère bien mon token, ma session SMTP s'ouvre sans erreurs, mais l'envoi du mail ne part pas.
ClientId est une chaîne = "111111-1111-11111"
ClientSecret est une chaîne = "1111~111111111111"
LocataireId est une chaîne = "11111111-1111-111"

MonTokenParam est un OAuth2Paramètres
MonTokenParam.ClientID = ClientId
MonTokenParam.ClientSecret = ClientSecret
MonTokenParam.URLAuth = "https://login.microsoftonline.com/"+ LocataireId +"/oauth2/v2.0/authorize"
MonTokenParam.URLToken = "https://login.microsoftonline.com/"+ LocataireId +"/oauth2/v2.0/token"
MonTokenParam.URLRedirection = "http://localhost:9874/"
MonTokenParam.Scope = "https://graph.microsoft.com/offline_access https://graph.microsoft.com/Mail.Send"
MonTokenParam.ParamètresSupplémentaires = "force_reapprove=false"
MonTokenParam.TypeRéponse = oauth2TypeRéponseCode

MaSessionSMTP est un emailSessionSMTP
//MaSessionSMTP.Nom = "username@domain.com"
MaSessionSMTP.AdresseServeur = "smtp.office365.com"
MaSessionSMTP.Port = 587
MaSessionSMTP.Option = emailProtocoleSMTPS
MaSessionSMTP.AuthToken = AuthIdentifie(MonTokenParam)

MonMessage est un Email
MonMessage.Expediteur = "username@domain.com"
Ajoute(MonMessage.Destinataire, "username@domain.com")
MonMessage.Sujet = "Sujet de test"
MonMessage.Message = "Titre Mon message de test"
MonMessage.HTML = "<h1>Titre</h1><p>Mon message de <b>test</b></p>"

SI EmailOuvreSession(MaSessionSMTP) = Faux ALORS
Erreur("Impossible d'ouvrir la session SMTP.", ErreurInfo())
RETOUR
FIN

SI EmailEnvoieMessage(MaSessionSMTP, MonMessage, emailOptionEncodeEntête) = Faux ALORS
Erreur(ErreurInfo(errRésumé))
FIN

EmailFermeSession(MaSessionSMTP)


Message d'erreur avec ce code :
"Le contenu de Email.Expéditeur n'est pas reconnu par le serveur. La transaction est refusée."

Et si je commente, j'ai ce message d'erreur :
"Vous avez appelé la fonction 'EmailEnvoieMessage'.
L'envoi d'un message sans préciser l'expéditeur n'est pas autorisé."

Avez-vous déjà réussi à envoyer un email via SMTP et une authentification avec oAuth2.0 pour Office 365 ? si oui comment ?

Merci par avance
Registered member
37 messages
Popularité : +1 (1 vote)
Posted on October, 19 2023 - 7:36 AM
Bonjour,
le champ expéditeur quand c'est un compte office doit forcément être renseigné avec l'adresse du titulaire du compte.

Pour ma part quand je passe par un SMTP avec office , je ne renseigne pas de Token , je rentre directement les informations IdUtilisateur et mot de passe du compte.
Le token n'est utilisé que si j'utilise les appels à l'api pour faire partir des mails en direct.

--
Cordialement
Registered member
19 messages
Posted on November, 02 2023 - 2:16 PM
Bonjour,

Je passe sur le forum pour un autre problème et je constate que j'ai reçu une réponse et une demande qui, pour moi, n'avaient pas été envoyées sur le forum car j'avais une erreur lorsque je postais le message... (Je pense que le forum a vraiment besoin d'une mise à jour !)

Entre-temps, j'ai trouvé la solution à mon problème sans avoir besoin de passer par les méthodes de Windev. J'ai dû reprendre le code d'un autre utilisateur sur le forum. Ensuite, j'ai créé une classe qui gère la génération du access_token et le refresh_token + autre classe qui gère le format attendu par l'API de Microsoft Graph et j'envoie mon mail de cette manière :

MonMail est un CLA_Mail
MonMail.subject("Sujet de test")
MonMail.content("<h1>Titre de test</h1><p>Mon contenu de test</p>")
MonMail.from(JSONVersChaîne(OAuthOffice365.User().mail))
MonMail.sender(JSONVersChaîne(OAuthOffice365.User().mail))
MonMail.toRecipients(["dest1@contact.com"])
MonMail.ccRecipients(["dest2@contact.com"; "dest3@contact.com"])
MonMail.bccRecipients(["dest4@contact.com"])
MonMail.attachmentAdd("PATH_DE_LA_PJ")

SI MonMail.send(OAuthOffice365.TokenSession.Valeur, Faux) ALORS
Info("Mail envoyé !")
SINON
Erreur("Mail non envoyé !")
FIN


La méthode send :
Procedure send(accessToken est une chaîne, saveToSentItems est un booléen = Vrai) <métier>

sMail est une chaîne

// Enregistre le mail dans le dossier des "Éléments envoyés" de l'expéditeur si Vrai
MonMail.saveToSentItems = saveToSentItems

Sérialise(MonMail,sMail,psdJSON)

maRequete est une httpRequête
maReponse est une httpRéponse
maRequete.Méthode = httpPost
maRequete.ContentType = "application/json"
maRequete.Entête["Authorization"] = "Bearer " + accessToken
maRequete.URL = "https://graph.microsoft.com/v1.0/me/sendMail"
maRequete.Contenu = sMail
maReponse = maRequete.Envoie()

SI maReponse.CodeEtat = 200 _OU_ maReponse.CodeEtat = 202 ALORS
RENVOYER Vrai
SINON
RENVOYER Faux
FIN
Posted on November, 17 2023 - 11:54 AM
Bonjour;

j'ai essayé la même chose en créant une librairie externe mais j'ai des soucis avec les dépendances pouvez-vous me montre en plusieurs détails votre CLA_Mail ? le token est valid combien de temps ?

Cordialement

Geremi
Registered member
19 messages
Posted on November, 19 2023 - 10:59 AM
Bonjour Gerem,

Code de la déclaration de ma classe CLA_Mail :
CLA_Mail est une Classe

MonMail est un STMail

FIN

STCorps est une structure
typeDucontenu est une chaîne <Sérialise="contentType"> // text ou html
content est une chaîne
FIN

STAdresse est une structure
Address est une chaîne
FIN

STEmailAdresse est une structure
EmailAddress est STAdresse
FIN

STFichierJoint est une structure
type est une chaîne <Sérialise="@odata.type"> // #microsoft.graph.fileAttachment
name est une chaîne
typeDucontenu est une chaîne <Sérialise="contentType">
///*
//text: données textuelles lisibles. text/rfc822 [RFC822] ; text/plain [RFC2646] ; text/html [RFC2854] .
//image: données binaires représentant des images numériques image/jpeg ; image/gif ; image/png.
//audio: données numériques sonores audio/basic ; audio/wav
//video : données vidéos : video/mpeg
//application : données binaires autres. application/octet-stream ; application/pdf
//*/
contentBytes est une chaîne
FIN

STMessage est une structure
subject est une chaîne
body est STCorps
from est un STEmailAdresse
sender est un STEmailAdresse
toRecipients est un tableau de STEmailAdresse
ccRecipients est un tableau de STEmailAdresse
bccRecipients est un tableau de STEmailAdresse
attachments est un tableau de STFichierJoint
FIN

STMail est une structure
monMessage est STMessage <Sérialise="message">
saveToSentItems est un booléen
FIN


Ensuite j'ai ajouté 9 méthodes à ma classe

Code de la méthode content :
Procedure content(valeur est une chaîne, type est une chaîne = "html") <métier>

MonMail.monMessage.body.content = valeur
MonMail.monMessage.body.typeDucontenu = type


Code de la méthode from :
Procedure from(valeur est une chaîne) <métier>

MonMail.monMessage.from.EmailAddress.Address = valeur


Code de la méthode sender :
Procedure sender(valeur est une chaîne) <métier>

MonMail.monMessage.sender.EmailAddress.Address = valeur


Code de la méthode toRecipients :
Procedure toRecipients(valeurs est un tableau de chaînes)

SI valeurs..Occurrence > 0 ALORS
POUR TOUT valeur de valeurs
SI valeur <> "" ALORS
recipient est un STEmailAdresse
recipient.EmailAddress.Address = valeur

MonMail.monMessage.toRecipients.Ajoute(recipient)
FIN
FIN
FIN


Code de la méthode ccRecipients :
Procedure ccRecipients(valeurs est un tableau de chaînes)

SI valeurs..Occurrence > 0 ALORS
POUR TOUT valeur de valeurs
SI valeur <> "" ALORS
recipient est un STEmailAdresse
recipient.EmailAddress.Address = valeur

MonMail.monMessage.ccRecipients.Ajoute(recipient)
FIN
FIN
FIN


Code de la méthode bccRecipients :
Procedure bccRecipients(valeurs est un tableau de chaînes)

SI valeurs..Occurrence > 0 ALORS
POUR TOUT valeur de valeurs
SI valeur <> "" ALORS
recipient est un STEmailAdresse
recipient.EmailAddress.Address = valeur

MonMail.monMessage.bccRecipients.Ajoute(recipient)
FIN
FIN
FIN


Code de la méthode bccRecipients :
Procedure attachmentAdd(pathFile est une chaîne, contentType est une chaîne = "application/pdf", name est une chaîne = "") <métier>

attachment est un STFichierJoint

SI pathFile <> "" OU Null ALORS
attachment.type = "microsoft.graph.fileAttachment"
SI name <> "" ALORS
attachment.name = name
SINON
attachment.name = fExtraitChemin(pathFile, fFichier+fExtension)
FIN
attachment.typeDucontenu = contentType
attachment.contentBytes = Encode(fChargeBuffer(pathFile), encodeBASE64)

MonMail.monMessage.attachments.Ajoute(attachment)
FIN


Code de la méthode subject :
Procedure subject(valeur est une chaîne) <métier>

MonMail.monMessage.subject = valeur


Code de la méthode send :
Procedure send(accessToken est une chaîne, saveToSentItems est un booléen = Vrai) <métier>

sMail est une chaîne

// Enregistre le mail dans le dossier des "Éléments envoyés" de l'expéditeur si Vrai
MonMail.saveToSentItems = saveToSentItems

Sérialise(MonMail,sMail,psdJSON)

maRequete est une httpRequête
maReponse est une httpRéponse
maRequete.Méthode = httpPost
maRequete.ContentType = "application/json"
maRequete.Entête["Authorization"] = "Bearer " + accessToken
maRequete.URL = "https://graph.microsoft.com/v1.0/me/sendMail"
maRequete.Contenu = sMail
maReponse = maRequete.Envoie()

SI maReponse.CodeEtat = 200 _OU_ maReponse.CodeEtat = 202 ALORS
RENVOYER Vrai
SINON
RENVOYER Faux
FIN


Il est facilement possible de faire un code plus propre mais pour mon cas d'utilisation cela me convient parfaitement.
Pour le token il est valide 2h mais je renouvelle le access_token avec le refresh_token si mon token expire du coup je n'ai pas besoin de me reconnecter à chaque fois.
Registered member
19 messages
Posted on November, 19 2023 - 11:09 AM
Je vous poste également ma classe qui gère le token et son renouvellement :

Code de déclaration de ma classe CLA_OAuthOffice365 :
CLA_OAuthOffice365 est une Classe

// Durée de vie du jeton de session :
// https://learn.microsoft.com/en-us/entra/identity-platform/configurable-token-lifetimes

CONSTANTE

// Les informations sont accessible depuis Microsoft Entra
// Dans la "Vue d'ensemble" de l'application inscrite
LOCATAIRE_ID = "xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx" // ID de l'annuaire (locataire) qui correspond à l'identifiant unique de la société
CLIENT_ID = "xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx" // ID d'application (client) à ne pas confondre avec les informations de "Secrets client"
CLIENT_SECRET = "" // Valeur du "Secret client"

URL_AUTH = "https://login.microsoftonline.com/"+ LOCATAIRE_ID +"/oauth2/v2.0/authorize"
URL_TOKEN = "https://login.microsoftonline.com/"+ LOCATAIRE_ID +"/oauth2/v2.0/token"
URL_REDIRECTION = "http://localhost:9874/"
SCOPE = "offline_access https://graph.microsoft.com/Mail.Send"
PARAMETRES_SUPPLEMENTAIRES = "force_reapprove=false"
FIN

MonTokenParam est un OAuth2Paramètres
TokenSession est un AuthToken
FichierPersistanceAuth est une chaîne
UserInfo est une JSON

FIN


Code du constructeur de la classe :
Procedure Constructeur()

FichierPersistanceAuth = fRepDonnées() + [fSep] + "AuthSession.bin"

MonTokenParam.ClientID = ::CLIENT_ID
//MonTokenParam.ClientSecret = ::CLIENT_SECRET
MonTokenParam.URLAuth = ::URL_AUTH
MonTokenParam.URLToken = ::URL_TOKEN
MonTokenParam.URLRedirection = ::URL_REDIRECTION
MonTokenParam.Scope = ::SCOPE
MonTokenParam.ParamètresSupplémentaires = ::PARAMETRES_SUPPLEMENTAIRES
MonTokenParam.TypeRéponse = oauth2TypeRéponseCode


Code de la méthode Connect :
Procedure Connect() : AuthToken

SI fFichierExiste(FichierPersistanceAuth) ALORS
QUAND EXCEPTIONEXCEPTION DANS
// Charge la session
getTokenSession()
FAIRE
// Erreur de relecture du token
ToastAffiche("Session oAuth mémorisée est invalide")
SINON
// Token est expiré ou va expirer dans la minute qui arrive
// et il est renouvelable ?
SI (TokenSession.DateExpiration-1Min < DateHeureSys()) _ET_
TokenSession.Actualisation <> "" ALORS
TokenSession = AuthRenouvelleToken(TokenSession)
SI TokenSession.Valide ALORS
// Sauvegarde du token
setTokenSession(TokenSession)
SINON
Erreur("Échec du renouvellement de la session oAuth")
FIN
FIN
FIN
FIN

SI PAS TokenSession.Valide ALORS
// Connexion au service
TokenSession = AuthIdentifie(MonTokenParam)
SI TokenSession.Valide ALORS
// Sauvegarde du token
setTokenSession(TokenSession)
FIN
FIN

RENVOYER TokenSession


Code de la méthode getTokenSession :
Procedure getTokenSession() <métier> : AuthToken

SI fFichierExiste(FichierPersistanceAuth) ALORS

bufToken est un Buffer

// Charge la session
bufToken = fChargeBuffer(FichierPersistanceAuth)
Désérialise(TokenSession, bufToken, psdBinaire)

FIN

RENVOYER TokenSession


Code de la méthode getTokenSession :
Procedure setTokenSession(AuthToken est un AuthToken) <métier> : booléen

bufToken est un Buffer

// Sauvegarde du token
Sérialise(AuthToken, bufToken, psdBinaire)

RENVOYER fSauveBuffer(FichierPersistanceAuth, bufToken)


Code de la méthode AuthSessionExiste :
Procedure PUBLIQUE GLOBALE AuthSessionExiste() <métier> : booléen

FichierPersistanceAuth est une chaîne = fRepDonnées() + [fSep] + "AuthSession.bin"

SI fFichierExiste(FichierPersistanceAuth) = Vrai ALORS
RENVOYER Vrai
FIN

RENVOYER Faux


Même chose pour cette classe le code peut facilement être améliorer mais cela me convient pour mon cas d'utilisation.

Je précise également que finalement le secret client n'est pas utile pour l'envoie de mail.
Registered member
417 messages
Popularité : +6 (6 votes)
Posted on November, 20 2023 - 8:20 AM
Bonjour et Merci Jérémie
Posted on November, 20 2023 - 12:01 PM
Merci de votre réponse rapide cela m'aide beaucoup ! :merci:
Registered member
24 messages
Popularité : +1 (1 vote)
Posted on March, 23 2024 - 7:13 AM
Merci Jérémie, bel esprit de partage, bien rare en francophonie... C'est exactement ce dont j'avais besoin :merci:

--
otto.matic@outlook.fr
Message modified, March, 23 2024 - 7:37 AM