summaryrefslogtreecommitdiff
path: root/imap-dl
diff options
context:
space:
mode:
Diffstat (limited to 'imap-dl')
-rwxr-xr-ximap-dl107
1 files changed, 86 insertions, 21 deletions
diff --git a/imap-dl b/imap-dl
index 469b81d..5a8494c 100755
--- a/imap-dl
+++ b/imap-dl
@@ -3,6 +3,7 @@
# -*- coding: utf-8 -*-
# Copyright (C) 2019-2020 Daniel Kahn Gillmor
+# Copyright (C) 2020 Red Hat, Inc.
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
@@ -17,22 +18,9 @@
# You should have received a copy of the GNU General Public License
# along with this program. If not, see <https://www.gnu.org/licenses/>.
-DESCRIPTION = '''A simple replacement for a minimalist use of getmail.
+DESCRIPTION = '''Fetch messages from an IMAP inbox into a maildir
-In particular, if you use getmail to reach an IMAP server as though it
-were POP (retrieving from the server and optionally deleting), you can
-point this script to the getmail config and it should do the same
-thing.
-
-It tries to ensure that the configuration file is of the expected
-type, and will terminate raising an exception, and it should not lose
-messages.
-
-If there's any interest in supporting other similarly simple use cases
-for getmail, patches are welcome.
-
-If you've never used getmail, you can make the simplest possible
-config file like so:
+Example config file:
----------
[retriever]
@@ -46,6 +34,8 @@ path = /home/foo/Maildir
[options]
delete = True
----------
+
+Run "man imap-dl" for more details.
'''
import re
@@ -61,13 +51,19 @@ import argparse
import statistics
import configparser
-from typing import Dict, List, Union, Tuple
+from typing import Dict, List, Optional, Tuple, Union
try:
import argcomplete #type: ignore
except ImportError:
argcomplete = None
+try:
+ import gssapi # type: ignore
+except ModuleNotFoundError:
+ gssapi = None
+
+
class Splitter(object):
def __init__(self, name:str, match:bytes):
self._splitter = re.compile(match)
@@ -91,6 +87,60 @@ summary_splitter = Splitter('summary', _summary_re)
_fetch_re = rb'^(?P<id>[0-9]+) \(UID (?P<uid>[0-9]+) (FLAGS \([\\A-Za-z ]*\) )?BODY\[\] \{(?P<size>[0-9]+)\}$'
fetch_splitter = Splitter('fetch', _fetch_re)
+def auth_builtin(username:str, imap:imaplib.IMAP4_SSL,
+ conf:configparser.ConfigParser, server:str) -> None:
+ logging.info('Logging in as %s', username)
+ resp:Tuple[str, List[Union[bytes,Tuple[bytes,bytes]]]]
+ resp = imap.login(username, conf.get('retriever', 'password'))
+ if resp[0] != 'OK':
+ raise Exception(f'login failed with {resp} as user {username} on {server}')
+
+# imaplib auth methods need to be in the form of callables, and they all
+# requre both additional parameters and storage beyond what the function
+# interface provides.
+class GSSAPI_handler():
+ gss_vc:gssapi.SecurityContext
+ username:str
+
+ def __init__(self, server:str, username:str) -> None:
+ name = gssapi.Name(f'imap@{server}', gssapi.NameType.hostbased_service)
+ self.gss_vc = gssapi.SecurityContext(usage="initiate", name=name)
+ self.username = username
+
+ def __call__(self, token:Optional[bytes]) -> bytes:
+ if token == b"":
+ token = None
+ if not self.gss_vc.complete:
+ response = self.gss_vc.step(token)
+ return response if response else b"" # type: ignore
+ elif token is None:
+ return b""
+
+ response = self.gss_vc.unwrap(token)
+
+ # Preserve the "length" of the message we received, and set the first
+ # byte to one. If username is provided, it's next.
+ reply:List[int] = []
+ reply[0:4] = response.message[0:4]
+ reply[0] = 1
+ if self.username:
+ reply[5:] = self.username.encode("utf-8")
+
+ response = self.gss_vc.wrap(bytes(reply), response.encrypted)
+ return response.message if response.message else b"" # type: ignore
+
+def auth_gssapi(username:str, imap:imaplib.IMAP4_SSL,
+ conf:configparser.ConfigParser, server:str) -> None:
+ if not gssapi:
+ raise Exception('Kerberos requested, but python3-gssapi not found')
+
+ logging.info(f'Logging in as {username} with GSSAPI')
+
+ callback = GSSAPI_handler(server, username)
+ resp = imap.authenticate("GSSAPI", callback)
+ if resp[0] != 'OK':
+ raise Exception(f'GSSAPI login failed with {resp} as user {username} on {server}')
+
def scan_msgs(configfile:str, verbose:bool) -> None:
conf = configparser.ConfigParser()
conf.read_file(open(configfile, 'r'))
@@ -127,16 +177,31 @@ def scan_msgs(configfile:str, verbose:bool) -> None:
'(found "{on_size_mismatch_str}")')
ctx = ssl.create_default_context(cafile=ca_certs)
+ ssl_ciphers = conf.get('retriever', 'ssl_ciphers', fallback=None)
+ if ssl_ciphers:
+ ctx.set_ciphers(ssl_ciphers)
+
server:str = conf.get('retriever', 'server')
with imaplib.IMAP4_SSL(host=server, #type: ignore
port=int(conf.get('retriever', 'port', fallback=993)),
ssl_context=ctx) as imap:
username:str = conf.get('retriever', 'username')
- logging.info('Logging in as %s', username)
- resp:Tuple[str, List[Union[bytes,Tuple[bytes,bytes]]]]
- resp = imap.login(username, conf.get('retriever', 'password'))
- if resp[0] != 'OK':
- raise Exception(f'login failed with {resp} as user {username} on {server}')
+ authentication:str = conf.get('retriever', 'authentication',
+ fallback='basic')
+ # FIXME: have the default automatically choose an opinionated
+ # best authentication method. e.g., if the gssapi module is
+ # installed and the user has a reasonable identity in their
+ # local credential cache, choose kerberos, otherwise, choose
+ # "basic".
+ if authentication in ['kerberos', 'gssapi']:
+ auth_gssapi(username, imap, conf, server)
+ elif authentication == 'basic':
+ auth_builtin(username, imap, conf, server)
+ else:
+ # FIXME: implement other authentication mechanisms
+ raise Exception(f'retriever.authentication should be one of:\n'
+ '"basic" or "gssapi" (or "kerberos"). Got "{authentication}"')
+
if verbose: # only enable debugging after login to avoid leaking credentials in the log
imap.debug = 4
logging.info("capabilities reported: %s", ', '.join(imap.capabilities))