Skip to content

Commit

Permalink
Merge pull request #24 from mathewmarcus/sasl_auth
Browse files Browse the repository at this point in the history
Sasl auth
  • Loading branch information
Gentux committed Jun 3, 2018
2 parents 6f2ee91 + 1884aa5 commit 2a318e2
Show file tree
Hide file tree
Showing 8 changed files with 133 additions and 7 deletions.
10 changes: 10 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ library:
* Read mail
* Flag mail (Read, Unread, Delete, etc…)
* Copy, Move and Delete mail
* Authenticate using SASL

You can read about my initial motivation to write this software
[here](http://romain.soufflet.io/bash/2014/07/11/Mail-Mail-and-mail-again-my-head-will-explode.html).
Expand Down Expand Up @@ -55,6 +56,15 @@ Then, configure imap-cli creating a configuration file in `~/.config/imap-cli` c
password = secret
ssl = True

Alternatively, for authentication via a SASL mechanism such as XOAUTH2:

[imap]
hostname = imap.example.org
username = username
sasl_auth = XOAUTH2
bearer_access_token = abcde12345
ssl = True

If you want to add a minimal autocompletion, you can copy **imapcli_bash_completion.sh** in the file
**/etc/bash_completion.d/imapcli** or simply source.

Expand Down
24 changes: 24 additions & 0 deletions docs/configuration.rst
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,30 @@ This file can contains the following options::
format_status = {directory:>20} : {count:>5} Mails - {unseen:>5} Unseen - {recent:>5} Recent
limit = 10

SASL Authentication
~~~~~~~~~~~~~~~~~~~~~~
In addition to the standard `LOGIN` authentication illustrated above, Imap-CLI also supports authentication via SASL. This can be specified in the **[imap]** section of the config::

[imap]
hostname = imap.example.org
username = username
sasl_auth = OAUTHBEARER
sasl_ir = abcde12345
ssl = True

Here `sasl_auth` is the authentication method and `sasl_ir` is the initial response (or the client response to the first server challenge). If the `sasl_ir` contains
non-printable characters, such as the SOH (start of heading) character, you may find it easier to generate the config file programatically.

Additionally, for SASL XOAUTH2 authentication, Imap-CLI can simply take a `bearer_access_token` instead of the `sasl_ir`, like so::

[imap]
hostname = imap.example.org
username = username
sasl_auth = XOAUTH2
bearer_access_token = abcde12345
ssl = True

Imap-CLI will then automatically construct the SASL XOAUTH2 initial response.

.. warning::

Expand Down
9 changes: 7 additions & 2 deletions imap_cli/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,8 @@ def change_dir(imap_account, directory, read_only=True):
return -1


def connect(hostname, username, password, port=None, ssl=True):
def connect(hostname, username, password=None, port=None, ssl=True,
sasl_auth=None, sasl_ir=None):
"""Return an IMAP account object (see imaplib documentation for details)
.. versionadded:: 0.1
Expand All @@ -57,7 +58,11 @@ def connect(hostname, username, password, port=None, ssl=True):
else:
log.debug('Connecting on {}'.format(hostname))
imap_account = imaplib.IMAP4(hostname, port)
imap_account.login(username, password)

if sasl_auth:
imap_account.authenticate(sasl_auth, lambda x: sasl_ir)
else:
imap_account.login(username, password)
return imap_account


Expand Down
25 changes: 21 additions & 4 deletions imap_cli/config.py
Original file line number Diff line number Diff line change
Expand Up @@ -110,10 +110,27 @@ def new_context_from_file(config_filename=None, encoding='utf-8',
# Account
config['username'] = config_reader.get('imap', 'username')

try:
config['password'] = config_reader.get('imap', 'password')
except configparser.NoOptionError:
config['password'] = getpass.getpass()
config['sasl_auth'] = (
config_reader.get('imap', 'sasl_auth')
if config_reader.has_option('imap', 'sasl_auth')
else None
)

if config['sasl_auth']:
config['sasl_ir'] = (
const.SASL_XOAUTH2_IR.format(config['username'],
config_reader
.get('imap',
'bearer_access_token'))
if config['sasl_auth'] == 'XOAUTH2' and
config_reader.has_option('imap', 'bearer_access_token')
else config_reader.get('imap', 'sasl_ir')
)
else:
try:
config['password'] = config_reader.get('imap', 'password')
except configparser.NoOptionError:
config['password'] = getpass.getpass()

config['hostname'] = config_reader.get('imap', 'hostname')
config['ssl'] = config_reader.getboolean('imap', 'ssl')
Expand Down
3 changes: 3 additions & 0 deletions imap_cli/const.py
Original file line number Diff line number Diff line change
Expand Up @@ -85,6 +85,9 @@
'UNSEEN',
]

# This SASL XOAUTH2 initial client response is documented in the Gmail API
# https://developers.google.com/gmail/imap/xoauth2-protocol#initial_client_response
SASL_XOAUTH2_IR = 'user={}\x01auth=Bearer {}\x01\x01'

# CLI Constant
DEFAULT_CONFIG_FILE = '~/.config/imap-cli'
61 changes: 61 additions & 0 deletions imap_cli/tests/test_config.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,9 @@


import json
from os import SEEK_SET
from six.moves import configparser
from tempfile import NamedTemporaryFile
import unittest

from imap_cli import config
Expand Down Expand Up @@ -59,3 +62,61 @@ def test_config_file_from_json(self):

for key, value in config.DEFAULT_CONFIG.items():
assert self.conf[key] == value


class SASLAuthConfigTest(unittest.TestCase):
def setUp(self):
self.config_file = NamedTemporaryFile('w+')
self.config_file.write('[imap]\n')
self.config_file.write('hostname = imap.example.org\n')
self.config_file.write('username = username\n')
self.config_file.write('ssl = True\n')

def test_xoauth2_with_bearer_access_token(self):
self.config_file.write('sasl_auth = XOAUTH2\n')
self.config_file.write('bearer_access_token = 12345abcde\n')
self.config_file.seek(SEEK_SET, 0)

self.conf = config.new_context_from_file(self.config_file.name,
section='imap')
assert self.conf['hostname'] == 'imap.example.org'
assert self.conf['username'] == 'username'
assert self.conf['sasl_auth'] == 'XOAUTH2'
assert self.conf['sasl_ir'] == \
'user=username\x01auth=Bearer 12345abcde\x01\x01'

def test_xoauth2_with_initial_response(self):
self.config_file.write('sasl_auth = XOAUTH2\n')
self.config_file.write('sasl_ir = 12345abcde\x01\n')
self.config_file.seek(SEEK_SET, 0)

self.conf = config.new_context_from_file(self.config_file.name,
section='imap')
assert self.conf['hostname'] == 'imap.example.org'
assert self.conf['username'] == 'username'
assert self.conf['sasl_auth'] == 'XOAUTH2'
assert self.conf['sasl_ir'] == '12345abcde\x01'

def test_other_sasl_auth_with_initial_response(self):
self.config_file.write('sasl_auth = OAUTHBEARER\n')
self.config_file.write('sasl_ir = 12345abcde\n')
self.config_file.seek(SEEK_SET, 0)

self.conf = config.new_context_from_file(self.config_file.name,
section='imap')
assert self.conf['hostname'] == 'imap.example.org'
assert self.conf['username'] == 'username'
assert self.conf['sasl_auth'] == 'OAUTHBEARER'
assert self.conf['sasl_ir'] == '12345abcde'

def test_sasl_auth_no_initial_response(self):
self.config_file.write('sasl_auth = XOAUTH2\n')
self.config_file.seek(SEEK_SET, 0)

self.assertRaises(configparser.NoOptionError,
config.new_context_from_file,
self.config_file.name,
section='imap')

def tearDown(self):
self.config_file.close()
6 changes: 6 additions & 0 deletions imap_cli/tests/test_imapcli.py
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,12 @@ def test_connect_no_ssl(self):
'password', ssl=False)
assert isinstance(self.imap_account, tests.ImapConnectionMock)

def test_connect_sasl_auth(self):
self.imap_account = imap_cli.connect('hostname', 'username',
sasl_auth='XOAUTH2',
sasl_ir='12345abcde')
assert isinstance(self.imap_account, tests.ImapConnectionMock)

def test_wrong_change_dir(self):
self.imap_account = imaplib.IMAP4_SSL()
self.imap_account.login()
Expand Down
2 changes: 1 addition & 1 deletion setup.py
Original file line number Diff line number Diff line change
Expand Up @@ -52,5 +52,5 @@
scripts=["imapcli"],
test_suite="nose.collector",
url="http://gentux.github.io/imap-cli/",
version="0.7",
version="0.8",
)

0 comments on commit 2a318e2

Please sign in to comment.