aboutsummaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorJason Gunthorpe <jgg@mellanox.com>2020-06-19 14:10:28 -0300
committerJason Gunthorpe <jgg@nvidia.com>2020-06-22 20:24:18 -0300
commitc607f4a3cbd9cc4c9611db12bfe175b52de514e1 (patch)
tree7d9772b095b956137cab0f17c8941fff2b33b08c
parent6c7dbe902d8679570ca10f39672d844fa5cb6c50 (diff)
downloadcloud_mdir_sync-c607f4a3cbd9cc4c9611db12bfe175b52de514e1.tar.gz
cloud_mdir_sync-c607f4a3cbd9cc4c9611db12bfe175b52de514e1.tar.bz2
cloud_mdir_sync-c607f4a3cbd9cc4c9611db12bfe175b52de514e1.zip
OAUTH: Add support to get an IMAP OAUTH token
Latest mutt can do this for MS and GMail providers, provide support for getting the right scope and some examples how to set it up. Signed-off-by: Jason Gunthorpe <jgg@mellanox.com>
-rw-r--r--README.md6
-rw-r--r--cloud_mdir_sync/cms_oauth_main.py50
-rw-r--r--cloud_mdir_sync/config.py13
-rw-r--r--cloud_mdir_sync/credsrv.py15
-rw-r--r--cloud_mdir_sync/gmail.py6
-rw-r--r--cloud_mdir_sync/oauth.py7
-rw-r--r--cloud_mdir_sync/office365.py10
-rw-r--r--doc/imap.md54
8 files changed, 130 insertions, 31 deletions
diff --git a/README.md b/README.md
index e20af76..b1ae07b 100644
--- a/README.md
+++ b/README.md
@@ -261,6 +261,12 @@ CMS includes a OAUTH broker than can export a SMTP access token to local SMTP
delivery agents. The [Outbound mail through SMTP](doc/smtp.md) page describes
this configuration.
+# OAUTH only
+
+CMS can also act as an OAUTH broker for other mail access programs, in this
+mode it does not handle email. The [Inbound mail through IMAP](doc/imap.md)
+page describes this configuration.
+
# Future Work/TODO
- Use delta queries on mailboxes with MS Graph. Delta queries allow
downloading only changed message meta-data and will accelerate polling of
diff --git a/cloud_mdir_sync/cms_oauth_main.py b/cloud_mdir_sync/cms_oauth_main.py
index c7c6699..514e411 100644
--- a/cloud_mdir_sync/cms_oauth_main.py
+++ b/cloud_mdir_sync/cms_oauth_main.py
@@ -1,6 +1,7 @@
# SPDX-License-Identifier: GPL-2.0+
import argparse
import base64
+import contextlib
import re
import socket
@@ -9,6 +10,11 @@ def get_xoauth2_token(args):
"""Return the xoauth2 string. This is something like
'user=foo^Aauth=Bearer bar^A^A'
"""
+ if args.test_smtp:
+ args.proto = "SMTP"
+ elif args.test_imap:
+ args.proto = "IMAP"
+
with socket.socket(socket.AF_UNIX, socket.SOCK_STREAM) as sock:
sock.connect(args.cms_sock)
sock.sendall(f"{args.proto} {args.user}".encode())
@@ -20,14 +26,27 @@ def get_xoauth2_token(args):
def test_smtp(args, xoauth2_token):
- """Initiate a testing SMTP connection to verify """
+ """Initiate a testing SMTP connection to verify the token and server
+ work"""
import smtplib
- conn = smtplib.SMTP(args.test_smtp, 587)
- conn.set_debuglevel(True)
- conn.ehlo()
- conn.starttls()
- conn.ehlo()
- conn.auth("xoauth2", lambda x: xoauth2_token, initial_response_ok=False)
+ with contextlib.closing(smtplib.SMTP(args.test_smtp, 587)) as conn:
+ conn.set_debuglevel(True)
+ conn.ehlo()
+ conn.starttls()
+ conn.ehlo()
+ conn.auth("xoauth2",
+ lambda x: xoauth2_token,
+ initial_response_ok=False)
+
+
+def test_imap(args, xoauth2_token):
+ """Initiate a testing IMAP connection to verify the token and server
+ work"""
+ import imaplib
+ with contextlib.closing(imaplib.IMAP4_SSL(args.test_imap)) as conn:
+ conn.debug = 4
+ conn.authenticate('XOAUTH2', lambda x: xoauth2_token.encode())
+ conn.select('INBOX')
def main():
@@ -35,7 +54,7 @@ def main():
parser.add_argument(
"--proto",
default="SMTP",
- choices={"SMTP"},
+ choices={"SMTP", "IMAP"},
help="""Select the protocol to get a token for. The protocol will
automatically select the correct OAUTH scope.""")
parser.add_argument(
@@ -57,19 +76,30 @@ def main():
xoauth2 is used if the caller will provide the base64 conversion.
token returns the bare access_token""")
- parser.add_argument(
+ tests = parser.add_mutually_exclusive_group()
+ tests.add_argument(
"--test-smtp",
metavar="SMTP_SERVER",
help=
"""If specified attempt to connect and authenticate to the given SMTP
- sever. This can be used to test that the authentication method works
+ server. This can be used to test that the authentication method works
properly on the server. Typical servers would be smtp.office365.com
and smtp.gmail.com.""")
+ tests.add_argument(
+ "--test-imap",
+ metavar="IMAP_SERVER",
+ help=
+ """If specified attempt to connect and authenticate to the given IMAP
+ server. This can be used to test that the authentication method works
+ properly on the server. Typical servers would be outlook.office365.com
+ and imap.gmail.com.""")
args = parser.parse_args()
xoauth2_token = get_xoauth2_token(args)
if args.test_smtp:
return test_smtp(args, xoauth2_token)
+ if args.test_imap:
+ return test_imap(args, xoauth2_token)
if args.output == "xoauth2-b64":
print(base64.b64encode(xoauth2_token.encode()).decode())
diff --git a/cloud_mdir_sync/config.py b/cloud_mdir_sync/config.py
index 79ab4a9..019e757 100644
--- a/cloud_mdir_sync/config.py
+++ b/cloud_mdir_sync/config.py
@@ -116,9 +116,18 @@ class Config(object):
self.local_mboxes.append(MailDirMailbox(self, directory))
return self.local_mboxes[-1]
- def CredentialServer(self, path: str, accounts: List, umask=0o600):
+ def CredentialServer(self,
+ path: str,
+ accounts: List,
+ umask=0o600,
+ protocols=["SMTP"]):
+ """Serve XOAUTH2 bearer tokens over a unix domain socket. The client
+ writes the user to obtain a token for and the server responds with the
+ token. protocols can be IMAP or SMTP. The cms-oauth program interacts
+ with this server."""
from .credsrv import CredentialServer
- self.async_tasks.append(CredentialServer(self, path, accounts, umask))
+ self.async_tasks.append(
+ CredentialServer(self, path, accounts, umask, protocols))
return self.async_tasks[-1]
def _direct_message(self, msg):
diff --git a/cloud_mdir_sync/credsrv.py b/cloud_mdir_sync/credsrv.py
index 97357bc..45a032f 100644
--- a/cloud_mdir_sync/credsrv.py
+++ b/cloud_mdir_sync/credsrv.py
@@ -11,18 +11,15 @@ from . import config, oauth
class CredentialServer(object):
"""Serve XOAUTH2 bearer tokens over a unix domain socket. The client
writes the user to obtain a token for and the server responds with the
- token"""
- def __init__(self,
- cfg: config.Config,
- path: str,
- accounts: List[oauth.Account],
- umask=0o600):
+ token. protocols can be IMAP or SMTP"""
+ def __init__(self, cfg: config.Config, path: str,
+ accounts: List[oauth.Account], umask, protocols):
self.cfg = cfg
self.path = os.path.abspath(os.path.expanduser(path))
self.umask = umask
self.accounts = {}
for I in accounts:
- I.oauth_smtp = True
+ I.protocols.update(protocols)
self.accounts[I.user] = I
async def go(self):
@@ -55,10 +52,10 @@ class CredentialServer(object):
f"Credential request {proto!r} {opts} {user!r}")
account = self.accounts.get(user)
- if account is None:
+ if account is None or proto not in account.protocols:
return
- xoauth2 = await account.get_xoauth2_bytes("SMTP")
+ xoauth2 = await account.get_xoauth2_bytes(proto)
if xoauth2 is None:
return
writer.write(xoauth2)
diff --git a/cloud_mdir_sync/gmail.py b/cloud_mdir_sync/gmail.py
index dc70361..20e7502 100644
--- a/cloud_mdir_sync/gmail.py
+++ b/cloud_mdir_sync/gmail.py
@@ -89,7 +89,7 @@ class GmailAPI(oauth.Account):
self.scopes = [
"https://www.googleapis.com/auth/gmail.modify",
]
- if self.oauth_smtp:
+ if "SMTP" in self.protocols or "IMAP" in self.protocols:
self.scopes.append("https://mail.google.com/")
self.redirect_url = cfg.web_app.url + "oauth2/gmail"
@@ -263,13 +263,13 @@ class GmailAPI(oauth.Account):
async def close(self):
await self.session.close()
- async def get_xoauth2_bytes(self, proto: str) -> bytes:
+ async def get_xoauth2_bytes(self, proto: str) -> Optional[bytes]:
"""Return the xoauth2 byte string for the given protocol to login to
this account."""
while self.api_token is None:
await self.authenticate()
- if proto == "SMTP":
+ if proto == "SMTP" or proto == "IMAP":
res = 'user=%s\1auth=%s %s\1\1' % (self.user,
self.api_token["token_type"],
self.api_token["access_token"])
diff --git a/cloud_mdir_sync/oauth.py b/cloud_mdir_sync/oauth.py
index 716e7c0..385e0e8 100644
--- a/cloud_mdir_sync/oauth.py
+++ b/cloud_mdir_sync/oauth.py
@@ -19,16 +19,15 @@ if TYPE_CHECKING:
class Account(object):
"""An OAUTH2 account"""
- oauth_smtp = False
def __init__(self, cfg: "config.Config", user: str):
self.cfg = cfg
self.user = user
+ self.protocols = set()
@abstractmethod
- async def get_xoauth2_bytes(self, proto: str) -> bytes:
- pass
-
+ async def get_xoauth2_bytes(self, proto: str) -> Optional[bytes]:
+ return None
class WebServer(object):
"""A small web server is used to manage oauth requests. The user should point a browser
diff --git a/cloud_mdir_sync/office365.py b/cloud_mdir_sync/office365.py
index f66882f..6fa6145 100644
--- a/cloud_mdir_sync/office365.py
+++ b/cloud_mdir_sync/office365.py
@@ -120,10 +120,14 @@ class GraphAPI(oauth.Account):
self.session = aiohttp.ClientSession(connector=connector,
raise_for_status=False)
- if self.oauth_smtp:
+ if "SMTP" in self.protocols:
self.owa_scopes = self.owa_scopes + [
"https://outlook.office.com/SMTP.Send"
]
+ if "IMAP" in self.protocols:
+ self.owa_scopes = self.owa_scopes + [
+ "https://outlook.office.com/IMAP.AccessAsUser.All"
+ ]
self.redirect_url = self.cfg.web_app.url + "oauth2/msal"
self.oauth = oauth.OAuth2Session(
@@ -452,13 +456,13 @@ class GraphAPI(oauth.Account):
async def close(self):
await self.session.close()
- async def get_xoauth2_bytes(self, proto: str) -> bytes:
+ async def get_xoauth2_bytes(self, proto: str) -> Optional[bytes]:
"""Return the xoauth2 byte string for the given protocol to login to
this account."""
while self.owa_token is None:
await self.authenticate()
- if proto == "SMTP":
+ if proto == "SMTP" or proto == "IMAP":
res = 'user=%s\1auth=%s %s\1\1' % (self.user,
self.owa_token["token_type"],
self.owa_token["access_token"])
diff --git a/doc/imap.md b/doc/imap.md
new file mode 100644
index 0000000..71efef7
--- /dev/null
+++ b/doc/imap.md
@@ -0,0 +1,54 @@
+# Inbound mail through IMAP
+
+While CMS will not use IMAP directly, it can act as an OAUTH authentication
+broker for other mail clients. In this mode CMS would be configured to only do
+authentication and not handle mail.
+
+## Authenticate only CMS Configuration
+
+In this mode no mailboxes are defined, just accounts and the CredentialServer
+
+```Python
+account = Office365_Account(user="user@domain.com")
+CredentialServer("/var/run/user/XXX/cms.sock",
+ accounts=[account],
+ protocols=["SMTP", "IMAP"])
+```
+
+CMS will still run as a daemon and it keeps track of the refresh token and
+periodically updates the access tokens.
+
+## Configuration Test
+
+CMS provides the *cms-auth* tool to get tokens out of the daemon. It has a
+test mode which should be used to verify that the IMAP server is working correctly:
+
+```sh
+$ cms-oauth --user=user@domain.com --cms_sock=/var/run/user/XXX/cms.sock --test-imap=outlook.office365.com
+```
+
+On success their should be a log something like:
+
+```
+ 40:51.37 < b'NDNI1 OK AUTHENTICATE completed.'
+```
+
+# mutt
+
+Since Mutt 1.11 it has support for OAUTHBEARER authentication. This can be
+used with GMail and CMS. The below fragment of the .mutt RC shows the configuration.
+
+```
+set imap_authenticators="oauthbearer"
+set imap_oauth_refresh_command="cms-oauth --cms_sock=cms.sock --proto=IMAP --user user@domain --output=token"
+set spoolfile="imaps://imap.gmail.com/INBOX"
+```
+
+As of mutt commit c7a872d1eeea ("Add basic XOAUTH2 support.") (possibly will
+be in version 1.15) mutt can also do XOAUTH2 for use with Office365:
+
+```
+set imap_authenticators="xoauth2"
+set imap_oauth_refresh_command="cms-oauth --cms_sock=cms.sock --proto=IMAP --user user@domain --output=token"
+set spoolfile="imaps://outlook.office365.com/INBOX"
+```