Replies:

Can be rewritten with object-script, but for me it is fine. No external command nor external files. ClassMethods can used for an authorization.

Perhaps Intersystems will be produce a better solution for us...

Got EnsLib.EMail.InboundAdapter and added some line using AccessToken[Name]. Embedded Python code based on mutt_oauth2.py I have stripped it a little (only 'authcode'-flow) Elliminate external file and using IRIS-globals. Put 'client_id', 'client_secret', 'tenant' from registrations to token (saved in global) to be independent from edit sourcecode like mutt_oauth2.py was.

To use OAuth2 you have to start with getting an URL for authentication process:

w ##class(My.EnsLib.EMail.OAuth2InboundAdapter).askAuthorization(AccessTokenName, registration, mail, clientid, clientsecret [,tenant])

registration meens the name of the python 'registrations'-Array-Element.  The output is an URL to use in browser for getting an authorizationCode. After login and some confirmations you get a code. On google you find it much clear in a well styled webpage. On microsoft you find the code as part of the destination url in browsers url textfield.

w ##class(My.EnsLib.EMail.OAuth2InboundAdapter).doAuthorization(AccessTokenName, AuthorizationCode)

returns the first AccessToken and saving all details on ^OAuth2.AccessToken(AccessTokenName) global.

now you are ready to retrieve mails by oauth2.

Class My.EnsLib.EMail.OAuth2InboundAdapter Extends Ens.InboundAdapter [ ClassType = "", ProcedureBlock, System = 4 ]
{
Parameter SERVICEINPUTCLASS = "%Net.MailMessage"; 
Property POP3Server As %String; 
Property POP3Port As %Integer; 
/// Name of AccessToken for OAuth 2.0 in ^OAuth2.AccessToken (experimental)
Property AccessTokenName As %String;
/// Long-Live-AccessToken for OAuth 2.0 (experimental)
Property AccessToken As %String(MAXLEN = 2000);
Property MatchFrom As %String; 
Property MatchTo As %String; 
Property MatchSubject As %String; 
Property SSLConfig As %String; 
Property SSLCheckServerIdentity As %Boolean [ InitialExpression = 0 ];
Parameter SETTINGS = "POP3Server:Basic,POP3Port:Basic,AccessTokenName:Basic,AccessToken:Basic,Credentials:Basic:credentialsSelector,SSLConfig:Connection:sslConfigSelector,SSLCheckServerIdentity:Connection,MatchFrom,MatchTo,MatchSubject"; 
Property MailServer As %Net.POP3; 
Property %UIDArray [ MultiDimensional, Private ]; 
Property %UIDKey As %String; 
Property %ILastMsg As %Integer [ InitialExpression = 0 ]; 
Property %MsgsFound [ MultiDimensional, Private ]; 

Method OnInit() As %Status
{
    #; Set up POP MailServer object
    Do ..MailServerNewObject()  $$$ASSERT($IsObject(..MailServer))     #; If there is an SSL Configuration identified, see if it also wants to use STARTTLS (look for the '*')
    Set ..SSLConfig = $ZSTRIP(..SSLConfig,"<>WC")
    If (""'=..SSLConfig) { 
        Set ..MailServer.SSLConfiguration = $S("*"=$E(..SSLConfig,*):$E(..SSLConfig,1,*-1),1:..SSLConfig)
        Set ..MailServer.UseSTARTTLS = ("*"=$E(..SSLConfig,*))
        Set ..MailServer.SSLCheckServerIdentity = ..SSLCheckServerIdentity
        $$$EnsCheckSSLConfig(..SSLConfig)
    }
    Set ..%UIDArray=$$$NULLOREF
    Quit ##super()
} 

Method OnTask() As %Status
{
#define MsgTable(%msgid) $$$EnsRuntimeAppData(..BusinessHost.%ConfigName,%msgid)
    Set $ZT="Trap",tSC=$$$OK,tCurrMsgID="" 
    Do {
        $$$sysTRACE("..%UIDArray='"_..%UIDArray_"', ..%ILastMsg="_..%ILastMsg_", ..%UIDKey="_..%UIDKey_", ..%UIDArray.Count()="_$S($IsObject(..%UIDArray):..%UIDArray.Count(),1:0)_", ..MailServer.Connected="_..MailServer.Connected)

        #; (Re-)connect to the server if in clean state
        If '$IsObject(..%UIDArray) {
            $$$ASSERT(""=..%UIDKey&&(0=..%ILastMsg)&&'..MailServer.Connected)
            $$$sysTRACE("Connecting...")
            If '$IsObject(..%CredentialsObj) Do ..CredentialsSet(..Credentials) If '$IsObject(..%CredentialsObj) { Set tSC=$$$EnsError($$$EnsErrNoCredentials,..Credentials) Quit }
            Set tAccessToken=""
            Set:..AccessTokenName'="" tAccessToken=..getAccessToken(..AccessTokenName)
            If $FIND(tAccessToken,"ERROR:") = 7 {
                $$$LOGERROR(tAccessToken)
                Set tAccessToken=""
            }
            Set:tAccessToken="" tAccessToken=..AccessToken
            $$$TRACE("tAccessToken="_tAccessToken)
            Set tSC = ..MailServer.ConnectPort(..POP3Server,..POP3Port,..%CredentialsObj.Username,..%CredentialsObj.Password,tAccessToken)
            $$$SetJobMonitor(..BusinessHost.%ConfigName, $$$SystemName_":"_$Job,$$$eMonitorConnected,..MailServer.Connected_"|"_$$$timeUTC_"|"_..POP3Server_":"_..POP3Port)
            Set:$$$ISOK(tSC) tSC = ..MailServer.GetMessageUIDArray("",.tUIDArray) ; get results from UIDL command
            If $$$ISERR(tSC) || '$IsObject(tUIDArray) || (0=tUIDArray.Count()) {
                $$$sysTRACE("No Messages - Disconnecting...")
                If ..MailServer.Connected { Set tSCQuit = ..MailServer.QuitAndCommit()  Set:$$$ISOK(tSC) tSC = tSCQuit }
                If $$$ISERR(tSC) $$$LOGSTATUS(tSC)
                Quit
            } Else {
                If (0'=tUIDArray.Count()) { $$$sysTRACE("POP3 server reports "_tUIDArray.Count()_" messages in mailbox on server") }
            }
            Set ..%UIDArray=tUIDArray
            Kill ..%MsgsFound
        }
        #; Find the next one that can be processed
        For {
            Set ..%UIDKey = ..%UIDArray.Next(..%UIDKey), ..%ILastMsg=..%ILastMsg+1  Quit:""=..%UIDKey  ; done finding them
            Set tOneUID = ..%UIDArray.GetAt(..%UIDKey)  $$$ASSERT(""'=tOneUID)
            #; Get header, test for matching From,To, and/or Subject header
            Set tSC = ..MailServer.FetchMessageHeaders(..%UIDKey,.taMsgHeaders)  Quit:$$$ISERR(tSC)
            Set tOneMsgID=$G(taMsgHeaders("message-id")) If ""=tOneMsgID $$$LOGWARNING("Received message "_..%UIDKey_" with no message-id (Unable to lock or mark it errored), From: "_$G(taMsgHeaders("from"))_", Subject: "_$G(taMsgHeaders("subject")))
            Set:$LENGTH(tOneMsgID)>511 tOneMsgID="=-SHA-512-="_$Replace($System.Encryption.Base64Encode($System.Encryption.SHAHash(512, tOneMsgID)),$C(13,10),"") 

            #; Check for a matching message we can deal with
            $$$sysTRACE("Got msg header '"_tOneMsgID_"', test for hdrs match") ; - From: "_$G(taMsgHeaders("from"))_", To: "_$G(taMsgHeaders("to"))_", Subject: "_$G(taMsgHeaders("subject")))
            Set tFromMatch=(""=..MatchFrom) For i=1:1:$L(..MatchFrom,";") If $G(taMsgHeaders("from"))[$P(..MatchFrom,";",i) Set tFromMatch=1 Quit
            Set tToMatch=(""=..MatchTo) For i=1:1:$L(..MatchTo,";") If $G(taMsgHeaders("to"))[$P(..MatchTo,";",i) Set tToMatch=1 Quit
            Set tSubjectMatch=(""=..MatchSubject) For i=1:1:$L(..MatchSubject,";") If $G(taMsgHeaders("subject"))[$P(..MatchSubject,";",i) Set tSubjectMatch=1 Quit
            If tFromMatch && tToMatch && tSubjectMatch {
                If ""'=tOneMsgID {
                    Set ..%MsgsFound(tOneMsgID)=1
                    #; Check for previously locked or errored messages    
                    Lock +$$$MsgTable(tOneMsgID):0 Else  $$$LOGINFO("Skipping locked Message '"_tOneMsgID_"'") Continue
                    If $G($$$MsgTable(tOneMsgID),0) If $G(^(tOneMsgID,"wrn")) { Kill ^("wrn") $$$LOGWARNING("Skipping previously errored message '"_tOneMsgID_"'") } Lock -$$$MsgTable(tOneMsgID) Continue
                }
                $$$sysTRACE("Got matching msg - header '"_tOneMsgID_"', From: "_$G(taMsgHeaders("from"))_", To: "_$G(taMsgHeaders("to"))_", Subject: "_$G(taMsgHeaders("subject")))
                Set:""'=tOneMsgID $$$MsgTable(tOneMsgID)=1, ^(tOneMsgID,"wrn")=1
                Set tCurrMsgID=$S(""'=tOneMsgID:tOneMsgID,1:"x")
                Quit ; found a good one
            }
        }
        If ""=..%UIDKey || $$$ISERR(tSC) {
            #; Done with this UIDArray now; Disconnect
            $$$ASSERT($$$ISERR(tSC)||(""=tCurrMsgID && (..%ILastMsg-1=..%UIDArray.Count())))    

            $$$sysTRACE("Disconnecting...")
            If ..MailServer.Connected { Set tSCQuit = ..MailServer.QuitAndCommit()  Set:$$$ISOK(tSC) tSC = tSCQuit } 

            #; Remove errored UIDs from global if they no longer exist
            Set m="" For  Set m=$Order($$$MsgTable(m))  Quit:m=""  If '$G(..%MsgsFound(m),0) Kill $$$MsgTable(m) $$$sysTRACE("Removed absent message '"_m_"' from errored list")
            Kill ..%MsgsFound
            Set ..%UIDArray= $$$NULLOREF, ..%UIDKey="", ..%ILastMsg=0
            Quit
        }
        $$$ASSERT(tCurrMsgID'="") 

        #; call BusinessService for processing
        Set tSC = ..MailServer.Fetch(..%ILastMsg,.tMailMessage)  Quit:$$$ISERR(tSC) ; Get full message
        $$$LOGINFO("Processing Mail Message "_..%ILastMsg_"/"_..%UIDArray.Count()_":'"_tCurrMsgID_"' From: "_tMailMessage.From_", To: "_tMailMessage.To.GetAt(1)_", Subject: "_tMailMessage.Subject)
        Set tSC = ..BusinessHost.ProcessInput(tMailMessage)  Quit:$$$ISERR(tSC)
        #; Delete Message from Server
        Set tSC = ..MailServer.DeleteMessage(..%UIDKey)  Quit:$$$ISERR(tSC)
        #; Mark the Message Not Errored
        Kill:"x"'=tCurrMsgID $$$MsgTable(tCurrMsgID)
    } While 0
Exit
    Lock:""'=tCurrMsgID&&("x"'=tCurrMsgID) -$$$MsgTable(tCurrMsgID)
    $$$SetJobMonitor(..BusinessHost.%ConfigName, $$$SystemName_":"_$Job,$$$eMonitorConnected,..MailServer.Connected_"|"_$$$timeUTC_"|"_..POP3Server_":"_..POP3Port)
    Quit tSC
Trap
    Set $ZT="",tSC=$$$EnsSystemError
    #; Disconnect if needed
    Do:..MailServer.Connected ..MailServer.QuitAndCommit()
    Goto Exit
}

Method OnTearDown() As %Status
{
    Do:..MailServer.Connected ..MailServer.QuitAndCommit()
    $$$SetJobMonitor(..BusinessHost.%ConfigName, $$$SystemName_":"_$Job,$$$eMonitorConnected,..MailServer.Connected_"|"_$$$timeUTC_"|"_..POP3Server_":"_..POP3Port)
    Quit $$$OK
}

ClassMethod getAccessToken(pTokenName As %VarString) As %VarString
{
    Quit ..OAuth2Method(pTokenName)
}

ClassMethod askAuthorization(pTokenName As %VarString, pReg As %VarString = "", pMail As %VarString = "", pClientId As %VarString = "", pClientSecret As %VarString = "", pTenant As %VarString = "") As %VarString
{
    Quit ..OAuth2Method(pTokenName, 1, pReg, pMail, pClientId, pClientSecret, pTenant)
}

ClassMethod doAuthorization(pTokenName As %VarString, pAuthCode As %VarString = "") As %VarString
{
    Quit ..OAuth2Method(pTokenName, 1, , , , , , pAuthCode)
}

ClassMethod OAuth2Method(pTokenName As %VarString, pDoAuth As %VarString = "", pReg As %VarString = "", pMail As %VarString = "", pClientId As %VarString = "", pClientSecret As %VarString = "", pTenant As %VarString = "", pAuthCode As %VarString = "") As %VarString [ Internal, Language = python, Private ]
{
#   based on mutt_oauth2 https://github.com/muttmua/mutt/blob/master/contrib/mutt_oauth2.py
    import sys
    import json
    import urllib.parse
    import urllib.request
    import base64
    import secrets
    import hashlib
    from datetime import timedelta, datetime
    import iris 

    registrations = {
        'google': {
            'authorize_endpoint': 'https://accounts.google.com/o/oauth2/auth',
            'devicecode_endpoint': 'https://oauth2.googleapis.com/device/code',
            'token_endpoint': 'https://accounts.google.com/o/oauth2/token',
            'sasl_method': 'OAUTHBEARER',
            'scope': 'https://mail.google.com/',
            'redirect_uri': 'urn:ietf:wg:oauth:2.0:oob',
        },
        'microsoft': {
            'authorize_endpoint': 'https://login.microsoftonline.com/common/oauth2/v2.0/authorize',
            'devicecode_endpoint': 'https://login.microsoftonline.com/common/oauth2/v2.0/devicecode',
            'token_endpoint': 'https://login.microsoftonline.com/common/oauth2/v2.0/token',
            'sasl_method': 'XOAUTH2',
            'scope': ('offline_access https://outlook.office.com/IMAP.AccessAsUser.All https://outlook.office.com/POP.AccessAsUser.All https://outlook.office.com/SMTP.Send'),
            'redirect_uri': 'https://login.microsoftonline.com/common/oauth2/nativeclient',
        },
        'microsoft.localredirect': {
            'authorize_endpoint': 'https://login.microsoftonline.com/common/oauth2/v2.0/authorize',
            'devicecode_endpoint': 'https://login.microsoftonline.com/common/oauth2/v2.0/devicecode',
            'token_endpoint': 'https://login.microsoftonline.com/common/oauth2/v2.0/token',
            'sasl_method': 'XOAUTH2',
            'scope': ('offline_access https://outlook.office.com/IMAP.AccessAsUser.All https://outlook.office.com/POP.AccessAsUser.All https://outlook.office.com/SMTP.Send'),
            'redirect_uri': 'http://localhost/',
        },
    }

    token = {}


    t = iris.gref('^OAuth2.AccessToken')
    tJson=t[pTokenName]
    if tJson != '' and not(tJson is None):
        token = json.loads(tJson)


    def writetokenfile():
        t = iris.gref('^OAuth2.AccessToken')
        t[pTokenName] = json.dumps(token).encode()


    if not token:
        if pDoAuth == '':
            return('ERROR: No token') 

    if pDoAuth != '' and not(pDoAuth is None):
        if pReg != '' and not(pReg is None):
            token['registration'] = pReg
        token['authflow'] = 'authcode'
        if pMail != '' and not(pMail is None):
            token['email'] = pMail
        if pClientId != '' and not(pClientId is None):
            token['client_id'] = pClientId
        if pClientSecret != '' and not(pClientSecret is None):
            token['client_secret'] = pClientSecret
        if pTenant != '' and not(pTenant is None):
            token['tenant'] = pTenant
        token['access_token'] = ''
        token['access_token_expiration'] = ''
        token['refresh_token'] = ''
        writetokenfile() 

    if token['registration'] not in registrations:
        return('ERROR: Unknown registration')
    registration = registrations[token['registration']]
    authflow = token['authflow']
    baseparams = {'client_id': token['client_id']}
    # Microsoft uses 'tenant' but Google does not
    if 'tenant' in token:
        baseparams['tenant'] = token['tenant']


    def access_token_valid():
        '''Returns True when stored access token exists and is still valid at this time.'''
        token_exp = token['access_token_expiration']
        return token_exp and datetime.now() < datetime.fromisoformat(token_exp)


    def update_tokens(r):
        '''Takes a response dictionary, extracts tokens out of it, and updates token file.'''
        if 'access_token' in r:
            token['access_token'] = r['access_token']
        if 'expires_in' in r:
            token['access_token_expiration'] = (datetime.now() + timedelta(seconds=int(r['expires_in']))).isoformat()
        if 'refresh_token' in r:
            token['refresh_token'] = r['refresh_token']
        writetokenfile()


    if pDoAuth != '' and not(pDoAuth is None):
        p = baseparams.copy()
        p['scope'] = registration['scope'] 
        if authflow == 'authcode':
            verifier = secrets.token_urlsafe(90)
            challenge = base64.urlsafe_b64encode(hashlib.sha256(verifier.encode()).digest())[:-1]
            redirect_uri = registration['redirect_uri'] 

            p.update({'login_hint': token['email'],
                    'response_type': 'code',
                    'redirect_uri': redirect_uri,
                    'code_challenge': challenge,
                    'code_challenge_method': 'S256'})
            if pAuthCode == '':
                t = iris.gref('^OAuth2.AccessToken')
                t[pTokenName,'Verifier'] = verifier
                return('Visit displayed URL to retrieve authorization code: ' + registration["authorize_endpoint"] + '?' + urllib.parse.urlencode(p, quote_via=urllib.parse.quote)) 

            t = iris.gref('^OAuth2.AccessToken')
            verifier=t[pTokenName,'Verifier']
            authcode = pAuthCode
            errinfo='' 

            for k in 'response_type', 'login_hint', 'code_challenge', 'code_challenge_method':
                del p[k]
            p.update({'grant_type': 'authorization_code',
                    'code': authcode,
                    'client_secret': token['client_secret'],
                    'code_verifier': verifier})
            try:
                response = urllib.request.urlopen(registration['token_endpoint'],
                                                urllib.parse.urlencode(p).encode())
            except urllib.error.HTTPError as err:
                errinfo=str(err.code)+':'+str(err.reason)+' ::: '
                response = err
            response = response.read()
            response = json.loads(response)
            if 'error' in response:
                descr=''
                if 'error_description' in response:
                    descr=' -- '+response['error_description']
                return('ERROR: AUTH -- '+ errinfo + response['error'] + descr)
        else:
            return(f'ERROR: Unknown OAuth2 flow "{token["authflow"]}".') 

        update_tokens(response) 

    if not access_token_valid():
        if not token['refresh_token']:
            return('ERROR: No refresh token. First "authorize".')
        p = baseparams.copy()
        p.update({'client_secret': token['client_secret'],
                'refresh_token': token['refresh_token'],
                'grant_type': 'refresh_token'})
        errinfo=''
        try:
            response = urllib.request.urlopen(registration['token_endpoint'],
                                            urllib.parse.urlencode(p).encode())
        except urllib.error.HTTPError as err:
            errinfo=str(err.code)+':'+str(err.reason)+' ::: '
            response = err
        response = response.read()
        response = json.loads(response)
        if 'error' in response:
            descr=''
            if 'error_description' in response:
                descr=' -- '+response['error_description']
            return('ERROR: Perhaps refresh token invalid. First "authorize" -- '+errinfo+response['error']+descr)
        update_tokens(response) 

    return(token['access_token'])
}

}
Followers:
Dieter has no followers yet.
Following:
Dieter has not followed anybody yet.
Global Masters badges:
Dieter has no Global Masters badges yet.